Rysowanie figur

PSzczepan

Podstawową rzeczą od której trzeba zacząć jest ręczne wyrysowanie okręgu- jeżeli chcemy narysować figurę o wszystkich bokach równej długości, najłatwiej będzie wpisać ją w okrąg. Oczywiście okrąg można narysować za pomocą jednej procedury, ale to nie rozwiązuje problemu. Potrzebna procedura wygląda tak:

var x,y:integer;
i:real ;
 
// ...
 
repeat
  x:=round(sin(i)*40); {40 to promień okręgu};
  y:=round(cos(i)*40);
  i:=i+0.001;
 {wartość o jaką zmienia się kąt w radianach w każdym cyklu
 pętli- mniejsza wartość wolniejsze działanie, ale większa dokładność}
  putpixel(x,y,10); {rysowanie}
until  i>2*pi ;

Jak łatwo zauważyć łatwo ją zmodyfikować tak, aby rysowana była elipsa:

x:=round(sin(i)*40); {40 to promień poziomy};
y:=round(cos(i)*80);{80- pionowy}

O ile elipsę można w Pascalu narysować poleceniem ellipse, o tyle jeżeli chcemy żeby była pochylona o jakiś kąt, musimy sami wszysko zaprogramować. Aby uzyskać ten efekt należy skorzystać z równań obracających dany punkt wokół osi:

xz:=pi; {kąt o jaki elipsa ma być pochylona}
repeat
 
  x:=round(sin(i)*40);
  y:=round(cos(i)*80); {współrzędne normalnej elipsy}
  x1:=round(cos(xz)*x+cos(xz+90)*y)+200;
 {wartość 200 spowoduje rysowaniu na środku (mniej więcej) ekranu, 
zamiast w górnym lewym rogu}
  y1:=round(sin(xz)*x+sin(xz+90)*y)+200; {x1,y1- współrzędne pochylone}
  i:=i+0.001;
  putpixel(x1,y1,10);
 
until i>2*pi;

Mając te podstawy możemy narysować teraz np.: pięciokąt foremny. Nie musimy do tego rysować okręgu wystarczy, że wyliczymy 5 punktów na okręgu znajdujących się w równej odległości od siebie:

var pent:array [1..5] of array [1..2] of integer;
 
// ...
 
for x:=1 to 5 do begin 
  pent[x,1]:=round(sin(2*pi/5*x)*40)+200;  
  pent[x,2]:=round(cos(2*pi/5*x))*40)+200;
end;
 
moveto(pent[1,1],pent[1,2]);
 
for x:=2 to 5 do lineto(pent[x,1],pent[x,2]);
 
lineto(pent[1,1],pent[1,1]);

Jak widać ten sam pięciokąt możemy równie dobrze wpisać w elipsę (nie będzie wtedy foremny oczywiście), a nawet w pochyloną elipsę:

z:=pi;
 
for x:=1 to 5 do begin 
 
  x1:=round(sin(2*pi/5*x)*40);  
  y1:=round(cos(2*pi/5*x)*90);
  pent[x,2]:=round(sin(z)*x1+sin(z+90)*y1)+200;
  pent[x,1]:=round(cos(z)*x1+cos(z+90)*y1)+200;
 
end;

Na koniec jeszcze jeden ciekawy drobiazg, a mianowicie rysowanie pentagramu. Znając już powyższe reguły nie ma w tym nic trudnego- wystarczy narysować okrąg, wyliczyć pięć punktów jak w pięciokącie, jedynie linie trzeba rysować do co drugiego punktu:

moveto(pent[1,1],pent[1,2]);
lineto(pent[3,1],pent[3,2]);
lineto(pent[5,1],pent[5,2]);
lineto(pent[2,1],pent[2,2]);
lineto(pent[4,1],pent[4,2]);
lineto(pent[1,1],pent[1,2]);

Problem z tak narysowanym pentagramem jest taki, że jest on odwrócony. Najprościej przywrócić go do normalnej pozycji poprzez wyliczenie punktów zaczynając od punktu znajdującego się po przeciwległej stronie okręgu:

pent[x,1]:=round(sin(2*pi/5*x+pi)*40);  
pent[x,2]:=round(cos(2*pi/5*x+pi)*40);

To teraz pozostaje nam już tylko rysować pochylone pentagramy :)

Piotr Szczepaniec
oddball@enter.net.pl
Serwis OHP

5 komentarzy

Ja znalazlem gdzies tez dobry sposob na rysowanie okregu bez obliczen zmiennoprzecinkowych (dane wejsciowe to: xinit, yinit - srodek okregu; Radius - promien):

x:=0;
y:=Radius;
d:=3-2Radius;
Repeat
putpixel(xinit+x,yinit+y,10);
putpixel(xinit+x,yinit-y,10);
putpixel(xinit-x,yinit+y,10);
putpixel(xinit-x,yinit-y,10);
putpixel(xinit+y,yinit+x,10);
putpixel(xinit+y,yinit-x,10);
putpixel(xinit-y,yinit+x,10);
putpixel(xinit-y,yinit-x,10);
if(d < 0) then
d := d + 4
x +6
else
begin
d := d + 4 * ( x -y ) + 10;
y=y - 1;
end;
x = x + 1;
Until not (x <= y);

Cholera, jeblem sie ;) Dalem na linie bo to metoda tuz pod rysowaniem kolka w moim projekcie, ale moze tez sie przyda.
Daje okrag:

int xv, yv;
for (xv = -r; xv < r; xv++)
{
    yv = (int) round(sqrt((float) r * r - xv * xv));
    canvas.putPoint(x + xv, y + yv);
    canvas.putPoint(x + xv, y - yv);
    canvas.putPoint(x + yv, y + xv);
    canvas.putPoint(x - yv, y + xv);
};    

Rysowanie elipsy, okręgu.
Rysowanie okręgów z wykorzystaniem funkcji trygonometrycznych sin, cos jak w artykule jest bardzo nieefektywne i np. ATARI sobie z tym nie radził. Poniżej podaje szybki algorytm generowania elips i okręgów

Elipsa opisana jest następującymi równaniami parametrycznymi.
x(t)=a cos(t)+x0
y(t)=b sin(t)+y0

Po zróżniczkowaniu równań otrzymujemy.
dx/dt=-a sin(t)
dy/dt=b cos(t)

Po podstawieniu pierwszych do drugich mamy.
dx=-a/b(y(t)-y0)dt
dy=b/a(x(t)-x0)dt

Wiadomo, że:
x(t+dt)=x(t)+dx oraz; y(t+dt)=y(t)+dy

Po podstawieniu wyliczonych przyrostów do ostatnich równań dostajemy finalny wynik (!).
x(t+dt)=x(t)-a/b(y(t)-y0)dt
y(t+dt)=y(t)+b/a(x(t)-x0)dt

Powyższe równania możemy wykorzystać bezpośrednio do generowania punktów. Jeśli chcemy mieć okrąg z N=100 punktów wyliczamy przyrost parametru dt=2*pi/N, przyjmujemy warunki początkowe x(0)=a+x0, y(0)=y0 i liczymy kolejne punkty z finalnego wyniku (!).

Poniżej zamieszczam program wykorzystujący opisaną metodę. W programie wyznaczana jest większa liczba punktów N=100 niż jest zapamiętywana n=50 w celu zwiększenia dokładności. Punkty okręgu zapamiętywane są w tablicach x, y.

// Kolo.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <conio.h>
#include <stdio.h>
#include <math.h>

int _tmain(int argc, _TCHAR argv[])
{
FILE
Plik;
double dt, a, b, xz, yz;
double X1, Y1;
double x, y;
int N, n, i, j, k;

//Parametry rysowanego okręgu
//a,b promienie elipsy, xz,yz-położenie srodka, jeśli a=b=r dostajemy okrąg
a=16; b=5; xz=2; yz=10;
//N-Liczba kroków algorytmu numerycznego (od niego zależy dokładność wyznaczanych punkrów okregu)
//n-Liczna zapemiętywanych punktow okręgu  N>n
N=100; n=50;

k=floor((double)N/(double)(n-1)+0.5);
n=floor((double)N/(double)(k)+0.5)+1;
x= new double[n];
y= new double[n];

dt=2*3.14/N;
X1=a; Y1=0;
x[0]=X1+xz;y[0]=Y1+yz;
a=a/b; b=1/a;
j=1;
for(i=1;i<=N;i++){
    X1=X1-a*Y1*dt;
    Y1=Y1+b*X1*dt;
if(i%k==0){
    x[j]=X1+xz;
    y[j]=Y1+yz;
    j++;
}
}
x[n-1]=x[0];
y[n-1]=y[0];

// Open for write 
if( (Plik = fopen("Elipsa.txt","w")) == NULL ){
  printf("The file 'Elipsa.txt' was not opened\n" );
  delete x; delete y; return 1; 
}
else
  printf("The file 'Elipsa.txt' was opened\n" );

for(i=0;i<n;i++){
    fprintf(Plik,"%f %f\n", x[i],y[i]);
}

fclose(Plik);

delete x;
delete y;

return 0;

}

Prawdopodobnie podobna metoda wykorzystywana jest do obliczania FFT szybkiej transformaty Fouriera, w której kolejne punkty transformaty generowane są na podstawie poprzednich. Jeśli ktoś wie jak ona działa to niech się podzieli chętnie wysłucham.

Dobrze jest załadować sin(alfa) i cos(alfa) do tablicy, wtedy mamy szybciej obliczane wartości, a jeśli ktoś chce szybko narysować okrąg to znacznie szybszy jest poniższy kod nie zawierający funkcji trygonometrycznych:
int x;
int y;
int d;
x = x1;
d = x1<x2?1:-1;
float g = (float) (y2 - y1) / (x2 - x1);
while (x1<x2?x<=x2:x>=x2) {
y = (int) round(y1 + g (x - x1));
putPoint(x,y);
x += d;
};
y = y1;
g = (float) (x2 - x1) / (y2 - y1);
d = y1<y2?1:-1;
while (y1<y2?y<=y2:y>=y2) {
x = (int) round(x1 + g
(y - y1));
putPoint(x,y);
y += d;
};

Czyli rysuje na podstawie r:=sqrt(sqr(x)+sqr(y)).
Mając x (zaczynamy od x= r*-1, kończymy na x = r) obliczamy
y = sqrt(sqr(r)-sqr(x))
Oczywiscie narysujemy wtedy tylko od dolu lub gory, ale z pomoca przychodza operatory +/- i mozna w jednej petli narysowac kolo od wszystkich czterech stron ;)

Bardzo przydatne funkcje. Korzystam z nich bardzo często. A w połączeniu z assemblerowskim ryowaniem punktu [odpowiednik putpixel] to działa bardzo szybko. Teraz tylko jeszcze dodać obsługę myszki i można robić drugiego Photoshopa. :P Pozdrowienia.