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

Przykład C++

Epicykloida i hipocykloida Parametry krzywych i ich wczytywanie ze strumienia Implementacja równań krzywych w C++ Rysowanie krzywych Program w C++ Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Epicykloida i hipocykloida

Zadaniem programu jest rysowanie epicy­kloidy i hipocy­kloidy – krzywych mających zastoso­wanie w różnych zagadnie­niach techni­cznych. Obie krzywe opisuje koniec P ramienia przytwier­dzonego sztywno do okręgu (koła) toczącego się stycznie bez poślizgu po stałym okręgu. Gdy okręgi stykają się zewnę­trznie (rys. poniżej z lewej), krzywa jest epicykloidą, a gdy wewnę­trznie (rys. poniżej z prawej), hipocykloidą. W drugim przypadku promień rucho­mego okręgu powinien być mniejszy od promienia stałego okręgu.

Na rysunkach oznaczono przez O i S środki okręgów stałego i ruchomego, przez R i r ich promienie, zaś przez d długość ramienia SP przytwier­dzonego do toczącego się okręgu. Przyjęto ponadto, że środek O stałego okręgu jest jednocze­śnie środkiem układu współ­rzędnych i na początku ruchu środek S toczącego się okręgu leży na dodatniej półosi x, a punkt P po jego lewej stronie, gdy okręgi stykają się zewnętrznie, albo po prawej, gdy stykają się wewnętrznie. Przy tych założe­niach uzyskuje się równania epicykloidy

oraz równania hipocykloidy

Kształt krzywych zależy od stosunku R do r. Jeżeli R jest podzielne przez r, krzywa jest zamknięta, a jeżeli nie, zamyka się po pewnej liczbie obiegów ruchomego okręgu, gdy R/r jest liczbą wymierną, a nie zamyka się nigdy, gdy jest liczbą niewy­mierną. Rozróżnia się również epicy­kloidę i hipocy­kloidę zwyczajną, gdy punkt P leży na toczącym się okręgu, czyli gdy d=r, skróconą, gdy d<r, oraz wydłużoną, gdy d>r. Dla d<0 ustawienie początkowe punktu P jest przeciwne względem środka S, a dla d=0 krzywa redukuje się do okręgu.

Parametry krzywych i ich wczytywanie ze strumienia

Naturalnym oczekiwaniem jest, aby program na początku przebiegu wczytywał z klawia­tury parametry krzywej, którą ma narysować. Oprócz trzech wartości rzeczy­wistych R, r i d wymagana jest również informacja, która krzywa ma być zobrazo­wana. Przyjmiemy, że gdy na wejściu wystąpi znak 'e' lub 'E', będzie to epicy­kloida, a gdy 'h' lub 'H', hipocy­kloida. Znak ten zostanie w funkcji wczytywania parametrów przekształ­cony na wygo­dniejszą do użycia wartość logiczną. Pobrane dane zostaną zapamiętane w zmiennych globalnych:

double R;       // Promień stałego okręgu
double r;       // Promień ruchomego okręgu
double d;       // Długość ramienia
bool epic;      // Epicykloida (true), hipocykloida (false)

Wydaje się pożądane, by program sprawdzał poprawność wczyty­wanych danych, gdyż nieprawi­dłowe mogą doprowa­dzić do niedorze­cznego rysunku. Zaprezen­towana poniżej funkcja pobiera ze standar­dowego strumienia cin znak identyfi­kujący rodzaj krzywej oraz trzy liczby rzeczy­wiste określające jej parametry R, r i d. Pobieranie znaku jest powtarzane dopóty, dopóki nie jest on rozpozna­walny jako pożądana litera E lub H (mała lub duża). Na koniec funkcja zwraca wartość logiczną true, gdy operacja czytania powiodła się i pobrane dane są poprawne, bądź false, gdy zakoń­czyła się niepowo­dzeniem lub dane są niezgodne z założeniami.

bool parametry()
{
    cout << "Epicykloida/Hipocykloida\n------------------------\n";
    char c;
    do
    {
        cout << "E/H: ";
        cin >> c;
        cin.ignore(1000, '\n');
    } while (c != 'e' && c != 'E' && c != 'h' && c != 'H');
    epic = (c == 'e') || (c == 'E');
    cout << "R  : ";
    cin >> R;
    cout << "r  : ";
    cin >> r;
    cout << "d  : ";
    cin >> d;
    return !cin.fail() && (R > 0) && (r > 0) && (epic || (r < R));
}

Funkcja pobiera ze strumienia cin znak i trzy liczby rzeczy­wiste za pomocą operatora >>. Gdyby użytko­wnik zawsze postępował zgodnie z lapidar­nymi polece­niami wyświetla­nymi na ekranie, tj. najpierw naciskał klawisz E lub H i Enter, a potem trzykrotnie wpisywał stosowną liczbę i kończył jej zapis klawiszem Enter, kod funkcji można by uprościć. Jest to jednak wyideali­zowana sytuacja. Należy mieć świadomość, że operacja wprowa­dzania danych jest szcze­gólnie narażona na różne błędy z powodu nieuwagi lub zamierzo­nych działań użytkownika.

Występujące w funkcji parametry wywołanie metody (funkcji) ignore obiektu cin powoduje usunięcie (zignoro­wanie) pewnej liczby znaków z bufora klawiatury. Jej pierwszy argument określa maksy­malną liczbę znaków do usunięcia, zaś drugi – znak, na którym usuwanie ma się zakończyć, gdyby został napotkany wcześniej. Jeśli zatem użytko­wnik wpisze np. łańcuch Epicykloida i naciśnie Enter, program pobierze znak 'E' i zignoruje resztę łańcucha wraz ze znakiem końca wiersza '\n'. Gdyby pominąć wywołanie ignore, reszta łańcucha zostałaby wykorzy­stana w następnym odczycie jako wartość numeryczna parametru R, co skończy­łoby się niepowo­dzeniem. Strumień znalazłby się w stanie błędu i dalsze operacje na nim byłyby ignorowane, dopóki nie zosta­łaby wywołana metoda clear. Stan strumienia można sprawdzić, wywołując m.in. metodę fail, która zwraca true, gdy operacja na strumieniu zakończyła się błędem, lub false, gdy powiodła się. Metoda ta została użyta w instrukcji return. Wartość true wyrażenia !cin.fail() oznacza, że parametry krzywej zostały wczytane bezpro­blemowo, a wtedy sprawdzana jest ich popra­wność.

Bez wątpienia funkcja parametry nie jest doskonała, mogłaby np. precy­zyjnie identy­fikować błędy. Naszym celem nie jest jednak jej ulepszanie. Ważne, że spełnia zadanie, informując, czy wprowa­dzane dane są poprawne.

Implementacja równań krzywych w C++

Równania parametryczne epicykloidy i hipocy­kloidy podane na początku niniej­szej witryny określają zależność współrzę­dnych x, y punktów krzywej od parametru φ przebiega­jącego przedział od 0 do π. W języku C++ można je sformu­łować w postaci dwóch funkcji:

void epicykloida(double fi, double &x, double &y)
{
    x = (R + r) * cos(fi) - d * cos((R + r) * fi / r);
    y = (R + r) * sin(fi) - d * sin((R + r) * fi / r);
}

void hipocykloida(double fi, double &x, double &y)
{
    x = (R - r) * cos(fi) + d * cos((R - r) * fi / r);
    y = (R - r) * sin(fi) - d * sin((R - r) * fi / r);
}

Współrzędne punktu stanowiące wynik obliczeń są w obydwu funkcjach przypisy­wane argumentom x i y zdefiniowanym jako referencje (znak &, przekazy­wanie parame­trów przez referencję). Dzięli temu obliczane wartości są bezpośre­dnio umieszczane w argumen­tach wywołań tych funkcji.

Rysowanie krzywych

Rzeczywista krzywa, którą program ma zobrazować, mieści się w okręgu o środku (0,0) i promieniu R+r+|d|, gdy jest epicy­kloidą, bądź R–r+|d|, gdy jest hipocy­kloidą. Naryso­wanie jej na ekranie wymaga odpowie­dniego przeskalo­wania, przesu­nięcia początku układu do środka obszaru robo­czego okna grafi­cznego i odwrócenia osi y. Przy wyborze czynnika skalują­cego uwzglę­dniamy niewielki margines wokół krzywej, przyjmując na przykład, że ma się ona mieścić w okręgu o pro­mieniu równym 9/10 mniejszej odległości środka obszaru robo­czego od jego brzegu. Rzecz jasna, zamiast krzywej rysujemy łamaną o pewnej liczbie wierzchołków leżących na tej krzywej. Liczba ta powinna być nie za wielka, ale jednocze­śnie wystarcza­jąca, by rysunek wiernie oddawał kształt krzywej. Wydaje się, że przyjęcie iloczynu długości promienia okręgu, w którym krzywa się mieści, i liczby R/r (liczby obiegów toczącego się okręgu po stałym okręgu) jest satysfakcjo­nujące. Rozwa­żania te prowadzą do następu­jącej funkcji rysowania krzywej:

void rysunek()            // Wersja uproszczona
{
    int p = getmaxx() / 2;
    int q = getmaxy() / 2;
    int n = 9 * min(p, q) / 10;
    double skala = n / ((epic ? (R + r) : (R - r)) + fabs(d));
    void (*cykloida)(double, double&, double&) = epic ? epicykloida : hipocykloida;
    if (R > r)
        n = int(n * R / r);
    double x, y;
    cykloida(0, x, y);
    moveto(int(skala * x) + p, q - int(skala * y));
    for (int k = 1; k <= n; k++)
    {
        cykloida(2 * M_PI * k / n, x, y);
        lineto(int(skala * x) + p, q - int(skala * y));
    }
}

Warto zwrócić uwagę na zmienną cykloida zadekla­rowaną jako wskaźnik na funkcję typu void o trzech argumen­tach typu double, z których pierwszy jest przekazy­wany przez wartość, drugi i trzeci przez referencję. Taką samą sygna­turę (taką samą liczbę argu­mentów, ich typy i typ zwracanej wartości) mają funkcje epicy­kloidahipocy­kloida, toteż można ich nazwy, które są w istocie wskaźnikami na funkcje, przypi­sywać zmiennej cykloida, podobnie jak można przekazywać funkcję w argu­mencie innej funkcji. Nazwa cykloida staje się po takim przypi­saniu synonimem nazwy epicy­kloida lub hipocy­kloida, co pozwala na użycie tego samego kodu do rysowania różnych krzywych.

Program w C++

Można wreszcie zaprezentować pełny program, który wczytuje parametry krzywej (epicy­kloidy lub hipocy­kloidy) i rysuje ją, gdy wczytane dane są prawidłowe, lub wypisuje stosowny komunikat, gdy zawie­rają błąd. Funkcja rysowania krzywej została nieco rozbudo­wana, zasadne bowiem wydawało się pokazanie mniej intensy­wnym kolorem obydwu okręgów i ramienia przytwier­dzonego do rucho­mego okręgu. Oto kod źródłowy programu:

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

using namespace std;

double R;       // Promień stałego okręgu
double r;       // Promień ruchomego okręgu
double d;       // Długość ramienia
bool epic;      // Epicykloida (true), hipocykloida (false)

bool parametry()
{
    cout << "Epicykloida/Hipocykloida\n------------------------\n";
    char c;
    do
    {
        cout << "E/H: ";
        cin >> c;
        cin.ignore(1000, '\n');
    } while (c != 'e' && c != 'E' && c != 'h' && c != 'H');
    epic = (c == 'e') || (c == 'E');
    cout << "R  : ";
    cin >> R;
    cout << "r  : ";
    cin >> r;
    cout << "d  : ";
    cin >> d;
    return !cin.fail() && (R > 0) && (r > 0) && (epic || (r < R));
}

void epicykloida(double fi, double &x, double &y)
{
    x = (R + r) * cos(fi) - d * cos((R + r) * fi / r);
    y = (R + r) * sin(fi) - d * sin((R + r) * fi / r);
}

void hipocykloida(double fi, double &x, double &y)
{
    x = (R - r) * cos(fi) + d * cos((R - r) * fi / r);
    y = (R - r) * sin(fi) - d * sin((R - r) * fi / r);
}

void rysunek()
{
    int p = getmaxx() / 2;
    int q = getmaxy() / 2;
    int n = 9 * min(p, q) / 10;
    double skala = n / ((epic ? (R + r) : (R - r)) + fabs(d));
    setcolor(CYAN);
    circle(p, q, int(skala * R));
    int s = int(skala * (epic ? (R + r) : (R - r))) + p;
    circle(s, q, int(skala * r));
    moveto(s, q);
    linerel(int(skala * (epic ? -d : d)), 0);
    setcolor(WHITE);
    void (*cykloida)(double, double&, double&) = epic ? epicykloida : hipocykloida;
    if (R > r)
        n = int(n * R / r);
    double x, y;
    cykloida(0, x, y);
    moveto(int(skala * x) + p, q - int(skala * y));
    for (int k = 1; k <= n; k++)
    {
        cykloida(2 * M_PI * k / n, x, y);
        lineto(int(skala * x) + p, q - int(skala * y));
    }
}

int main()
{
    if (parametry())
    {
        initwindow(640, 480, epic ? "Epicykloida" : "Hipocykloida");
        rysunek();
        getch();
        closegraph();
        return 0;
    }
    else
    {
        cout << "Problem z danymi\n";
        return 1;
    }
}

A oto kilka okien graficznych z przykła­dowymi krzywymi wygenero­wanymi przez program dla różnych parametrów (okna konsoli zostały pominięte):

a) epicykloida dla R=r=d=2 (kardioida),

b) epicykloida dla R=2, r=0.4, d=0.75,

c) hipocykloida dla R=2, r=d=0.5 (asteroida),

d) hipocykloida dla R=2, r=0.4, d=0.75,

e) hipocykloida dla R=100, r=5, d=80.


Opracowanie przykładu: marzec 2019