Учебник по программированию.

Первые шаги. Язык программирования PascalABC.

 

 

Учебник по программированию. Создание сайтов. Первые шаги.
Предыдущий параграф Назад в содержание Следующий параграф


§21. Геометрические фигуры.

Круг и окружность.

Здесь необходимо сделать небольшое пояснение. В данном параграфе кругом будем считать геометрическую фигуру круглой формы и закрашенную одним цветом. Окружностью будем считать линию, которая огибает круг и замыкается сама в себя. Не вникайте особо в слова, а взгляните на следующий рисунок и всё станет ясно:



Рассмотрим процедуры рисования круга и окружности:

procedure DrawCircle(x,y,r: integer); рисует окружность с центром (x,y) и радиусом r. Ширина окружности будет зависеть от настроек пера.

procedure FillCircle(x,y,r: integer); рисует круг с центром (x,y) и радиусом r. В справочной системе про эту процедуру написано следующее: «заполняет внутренность окружности с центром (x,y) и радиусом r». На мой взгляд, начинающему программисту будет удобнее понимать, что данная процедура рисует именно круг определённого цвета, поэтому предлагаю на данном этапе именно так и понимать. Все остальные процедуры в данном параграфе будут так же описаны в моей интерпретации, т.к. это удобнее для понимания, и вы без особого труда сразу сможете начать ими пользоваться.

procedure Circle(x,y,r: integer); рисует круг и окружность с центром (x,y) и радиусом r одновременно.

Прежде чем привести пример использования данных процедур нам необходимо познакомиться с понятием «кисть». Дело в том, что все примитивы можно подразделить на закрашенные фигуры и фигуры, состоящие из линии. Например, круг закрашенная фигура, окружность фигура, состоящая из линии. Так вот все фигуры, состоящие из линии, рисуются пером, речь о котором шла в предыдущем параграфе, а все закрашенные фигуры закрашиваются  кистью. Т.е. круг рисуется кистью, а окружность рисуется пером. Именно это и нужно уяснить из всего вышесказанного в данном абзаце.

Здесь же стоит сказать, что при рисовании текста, кисть закрашивает фон позади символов, установленным цветом в свойствах кисти. Чуть позже будет приведён пример и это будет показано наглядно. Так же стоит отметить, что изначально цвет кисти установлен белый, поэтому позади текста, который мы уже выводили в предыдущем параграфе, никакого закрашивания фона не происходило.


Подпрограммы для работы с кистью.

procedure SetBrushColor(c: Color); устанавливает цвет кисти.

function BrushColor: Color; возвращает цвет кисти.

procedure SetBrushStyle(bs: BrushStyleType); устанавливает стиль кисти.

function BrushStyle: BrushStyleType; возвращает стиль кисти.

Далее приведены константы стилей кисти:

  • bsSolid сплошная кисть (по умолчанию);
  • bsClear прозрачная кисть (на этом фоне будут видны предметы, которые были нарисованы ранее);
  • bsHatch штриховая кисть;
  • bsGradient градиентная кисть.

procedure SetBrushHatch(bh: HatchStyle); устанавливает штриховку текущей кисти.

function BrushHatch: HatchStyle; возвращает штриховку текущей кисти.

Далее константы стилей штриховки кисти:

  • bhHorizontal;
  • bhVertical;
  • bhForwardDiagonal;
  • bhBackwardDiagonal;
  • bhCross;
  • bhDiagonalCross;
  • bhPercent05

Стилей штриховки достаточно много, поэтому все их приводить не буду. Все стили можно просмотреть в справке в пункте: «Стандартные модули -> Модуль GraphABC -> GraphABC стили штриховки кисти». Или при наборе кода наберите bh и в всплывшей подсказке они все появятся.

Теперь можно привести пример рисования круга и окружности, а так же пример использования штриховки:

uses GraphABC;

begin

  SetBrushColor(clYellow);

  SetPenWidth(4);

//Первыя фигура

  TextOut(1,1,'FillCircle(50,85,40);');

  FillCircle(50,85,40);

//Вторая фигура

  TextOut(150,1,'Circle(200,85,40)');

  Circle(200,85,40);

//Третья фигура

  TextOut(300,1,'FillCircle(360,85,40);');

  TextOut(300,20,'DrawCircle(360,85,40);');

  FillCircle(360,85,40);

  DrawCircle(360,85,40);

//Четвёрнтая фигура

  SetBrushStyle(bsHatch);

  TextOut(450,10,'Использование штриховки.');

  Circle(520,85,40);

end.


Обратите внимание на то, что над каждой фигурой (кроме последней) подписан код, с помощью которого была нарисована данная фигура. А так же на то, что штриховка производится чёрными линиями, а не жёлтыми. В GraphABC штриховка бывает только чёрная.


Дуга. Часть круга.

procedure Arc(x,y,r,a1,a2: integer); рисует дугу окружности с центром в точке (x,y) и радиусом r, заключенной между двумя лучами, образующими углы a1 и a2 с осью OX (a1 и a2 вещественные, задаются в градусах и отсчитываются против часовой стрелки).

procedure FillPie(x,y,r,a1,a2: integer); заполняет внутренность сектора окружности, ограниченного дугой с центром в точке (x,y) и радиусом r, заключенной между двумя лучами, образующими углы a1 и a2 с осью OX (a1 и a2 вещественные, задаются в градусах и отсчитываются против часовой стрелки).

procedure Pie(x,y,r,a1,a2: integer); рисует заполненный сектор окружности, ограниченный дугой с центром в точке (x,y) и радиусом r, заключенной между двумя лучами, образующими углы a1 и a2 с осью OX (a1 и a2 вещественные, задаются в градусах и отсчитываются против часовой стрелки).

Прямоугольник.

procedure DrawRectangle(x1,y1,x2,y2: integer); рисует контур прямоугольника.

procedure FillRect(x1,y1,x2,y2: integer); рисует прямоугольник.

procedure Rectangle(x1,y1,x2,y2: integer); рисует контур и прямоугольник одновременно.

Во всех этих процедурах (x1,y1) координаты левого верхнего угла, (x2,y2) координаты левого нижнего угла прямоугольника. Небольшое уточнение контур прямоугольника рисуется пером, сам прямоугольник кистью.

Прямоугольник со скруглёнными краями.

procedure DrawRoundRect(x1,y1,x2,y2,w,h: integer); рисует контур прямоугольника со скругленными краями.

procedure FillRoundRect(x1,y1,x2,y2,w,h: integer); рисует прямоугольник со скругленными краями.

procedure RoundRect(x1,y1,x2,y2,w,h: integer); рисует контур и прямоугольник со скругленными краями

Здесь (x1,y1) и (x2,y2) координаты верхнего левого и нижнего правого углов соответственно прямоугольника, из которого получается прямоугольник со скруглёнными краями. w и h ширина и высота эллипса, используемого для скругления краев.

Эллипс.

procedure DrawEllipse(x1,y1,x2,y2: integer); рисует контур эллипса.

procedure FillEllipse(x1,y1,x2,y2: integer); рисует эллипс.

procedure Ellipse(x1,y1,x2,y2: integer); рисует контур эллипса и сам эллипс.

Здесь (x1,y1) и (x2,y2) координаты левого верхнего и правого нижнего углов прямоугольника, который ограничивает рисуемы эллипс.

Далее пример, который демонстрирует работу процедур рисующих, часть круга, прямоугольника, прямоугольника со скруглёнными краями и эллипса:


uses GraphABC;

begin

  SetBrushColor(clYellow);

  SetPenWidth(4);

//Полугруг

  Arc(50,50,40,90,180);

  FillPie(120,50,40,90,180);

  Pie(190,50,40,90,180);

//Прямоугольник 

  DrawRectangle(10,60,70,80);

  FillRect(80,60,140,80);

  Rectangle(150,60,210,80);

//Прямоугольник со скруглёнными краями

  DrawRoundRect(10,90,70,110,20,5);

  FillRoundRect(80,90,140,110,20,5);

  RoundRect(150,90,210,110,20,5);

//Эллипс

  DrawEllipse(10,120,70,140);

  FillEllipse(80,120,140,140);

  Ellipse(150,120,210,140);

end.


Заливка области заданным цветом.

Существует ещё одна хорошая процедура:

procedure FloodFill(x,y: integer; c: Color); заливает определённым цветом замкнутую область, в которой находится точка с координатами x,y. Действие данной процедуры схоже с действием инструмента «Заливка» в стандартном приложении «Paint», которое есть в любом Windows. Например, нарисуем несколько кругов наезжающих друг на друга жёлтого цвета. Затем с помощью процедуры FloodFill перерисуем получившийся рисунок коричневым цветом. Так же стоит сказать, что данной процедурой можно установить цвет фона перед началом рисования. Пример:


uses GraphABC;

begin

  FloodFill(1,1,clRed);

  SetBrushColor(clBrown);

  FillCircle(40,30,20);

  FillCircle(70,30,20);

  FillCircle(100,30,20);

  FloodFill(40,40,clYellow);

end.


Движущийся предмет.

Мы научились рисовать различные предметы. Теперь заставим предмет двигаться по экрану. В качестве примера нарисуем круг и заставим его двигаться по прямоугольной траектории. Для этого нам понадобятся две глобальные переменные x и y для хранения и изменения координат круга, переменная napr, которая будет хранить текущее направление движения, и следующие процедуры: процедуры рисования круга, стирания круга и процедура формирования координат круга для осуществления движения.

С процедурой рисования, думаю, всё понятно. А по поводу процедуры стирания сделаем пояснение: для того, что бы стереть круг в нашем случае необходимо нарисовать такой же круг, но цвета окружающего фона.

Так же сделаем пояснение по поводу процедуры изменения координат круга. Так как траектория движения круга будет проходить по четырём сторонам прямоугольника, то соответственно у нас будет 4 направления движения. В зависимости от направления будет меняться координата x или y. При достижении угла прямоугольника, направление движения будет меняться на следующее, соответственно после четвёртого направления снова будет первое.

Сам эффект движения круга получим по следующему алгоритму:

  • рисуем круг с координатами x,y;
  • выдерживаем паузу;
  • стираем круг;
  • меняем координаты x,y;
  • повторяем всё сначала.

Далее код программы полученный на основе приведённых рассуждений.


program DvijushiysaKrug;

uses GraphABC;

var x,y,//координаты круга

    napr://направление движения круга

    integer;


//Процедура рисования круга

procedure DrawKrug;

begin

  SetBrushColor(clYellow);

  FillCircle(x,y,20); 

end;


//Процедура стирания круга

procedure ClearKrug;

begin

  SetBrushColor(clWhite);

  FillCircle(x,y,20); 

end;

 

//Процедура формирования координат

procedure Koordinati;

begin

  if napr=1 then Inc(x);

  if napr=2 then Inc(y);

  if napr=3 then Dec(x);

  if napr=4 then Dec(y);

  if (napr=1) and (x=400) then napr:=2;

  if (napr=2) and (y=100) then napr:=3;

  if (napr=3) and (x=30) then napr:=4;

  if (napr=4) and (y=30) then napr:=1;

end;


begin

  x:=30; y:=30; napr:=1;

  while true do

   begin

    DrawKrug;

    Sleep(1);

    ClearKrug;

    Koordinati;

   end;

end.


Как видно из рисунка, результата работы программы, за кругом тянется шлейф. Это потому что не все крайние пиксели стирающего белого круга совпадают с пикселями старого жёлтого круга. Это потому, что при прорисовки круга, во время вычислений получаются вещественные числа, которые затем округляются до ближайшего целого. Поэтому при стирании круга по краю могут остаться пиксели от старого. Эти пиксели складываются, и получается подобный шлейф. Этот же эффект можно наблюдать и в предыдущем примере, где рассматривалась процедура FloodFill. Для устранения шлейфа сделаем радиус белого круга на один пиксель больше.

Так же если вы набрали текст программы и запустили её на выполнение, то вы должны были заметить, что круг как бы мерцает. Это происходит потому, что после стирания жёлтого круга компьютер делает вычисления координат, соответственно получается задержка, и время, при котором жёлтый круг стёрт, увеличивается. Плюс к этому компьютер производит кучу всяких вычислений, о которых мы даже можем и не подозревать. Отсюда и появляется эффект мерцания.

Для устранения мерцания необходимо изображение в начале сформировать во внутренней  памяти компьютера, а затем вывести его на экран. В таком случае изображение будет выведено без промежуточных вычислении, соответственно намного быстрее. Такой метод требует больше ресурсов компьютера, т.к. мы проделываем как бы двойную работу, зато мы избавляемся от мерцания. Для осуществления такого подхода существуют следующие процедуры:

procedure LockDrawing; блокирует рисование на экране. Другими словами данная процедура осуществляет переключение способа рисования, и рисование после вызова данной процедуры будет осуществляться во внутренней памяти. Соответственно, всё что мы нарисуем с помощью уже изученных процедур на экране не появится, а будет нарисовано во внутренней памяти.

Для того, что бы наши рисунки появились на экране необходимо вызвать следующую процедуру:

procedure Redraw; выводит на экран всё то, что было нарисовано во внутренней памяти.

Вернуть способ рисования непосредственно на экране можно следующей процедурой:

procedure UnlockDrawing; снимает блокировку рисования на экране и выводит на экран всё то, что было нарисовано во внутренней памяти.

Прежде чем привести пример усовершенствованной программы, скажу пару слов о том, как можно ускорить движение круга. Это можно сделать, уменьшив паузу, однако нужной скорости при этом может не получиться, т.к. минимальная пауза равна единице, и меньше сделать её нельзя. Второй способ это при минимальной паузе увеличить изменение координат. Например, в следующем примере будем увеличивать координаты не на единицу, а на два. Пример:


program DvijushiysaKrug;

………………………………………………………………

//Процедура стирания круга

procedure ClearKrug;

begin

  SetBrushColor(clWhite);

  FillCircle(x,y,21); 

end;

 

//Процедура формирования координат

procedure Koordinati;

begin

  if napr=1 then x:=x+2;

  if napr=2 then y:=y+2;

  if napr=3 then x:=x-2;

  if napr=4 then y:=y-2;

  …………………………………………………………

end;


begin

  x:=30; y:=30; napr:=1;

  LockDrawing;

  while true do

   begin

    DrawKrug;

    Redraw;

    Sleep(1);

    ClearKrug;

    Koordinati;

   end;

end.


В целях экономии бумаги, то, что осталось неизменным, заменено многоточием. Если вы внесли изменения и запустили программу, то вы должны были увидеть, что круг стал двигаться быстрее и шлейфа за ним не стало.


В данном параграфе мы научились выводить на экран геометрические фигуры и закрашивать замкнутую область определённым цветом. А так же познакомились с кистью и заставили предмет двигаться по экран.


Задачи.

1. Используя процедуру FloodFill и процедуры настройки кисти, перечертить чертёж из второго задания 20-ого параграфа следующим образом:



2. Используя графические примитивы, написать процедуры рисования следующих предметов: домик; дерево; мельница;  машинка. Процедуры поместить в отдельный модуль. Рисунок должен рисоваться в относительных координатах. Так же рисунок должен быть вписан в прямоугольник размером 30х40 пикселей. Пояснение: сам прямоугольник рисовать не нужно, рисунок должен иметь такие размеры, что бы он входил в этот невидимый прямоугольник и ни один его элемент не выходил  бы за пределы этого прямоугольника. По поводу относительных координат: смысл в том, что при вызове процедуры, рисующей рисунок, в качестве параметров должны передаваться координаты x,y. Например, координаты левого верхнего угла прямоугольника, в который вписан рисунок. При этом процедура должна нарисовать рисунок относительно этих координат, которые были переданы в качестве параметров. Дополнение: так же по желанию можете придумать и нарисовать свой предмет. У мельницы можно сделать крутящиеся лопасти.

3. Расположить в клиентской части окна предметы, которые были созданы при решении предыдущей задачи, и заставить машинку объехать все эти предметы. Примечание: для создания траектории движения машинки, можно создать массив координат опорных точек. При этом машинка должна будет двигаться от одной точки к другой. Для расчёта непосредственно координат для движения воспользуйтесь следующими выражениями:

  • уравнение прямой линии на плоскости, где и коэффициенты;
  • для нахождения коэффициентов, подставим в уравнение координаты (x1,y1) исходной точки, из которой начинается движение, и координаты (x2,y2) точки, к которой необходимо двигаться. Получим систему уравнений: .
  • Из этой системы можно найти коэффициенты: .


Решение.

1.

Program Chertej;

uses GraphABC;

const m=2;//Массштаб

      X0=10;Y0=10;//Начало координат

var tp:record //Текущая позиция дл процедуры LineFT

    x,y:integer;end;


//Line From tp To x,y

//Рисует линию от текущей позиции в точку (x,y),

//которая задаётся относитльно текущей позици

procedure LineFT(x,y:integer);

  ...................................


begin

//Рисуем основной контур толстыми линиями

  ...................................

//Дорисовываем толстые линии

  Line(X0+10,Y0+51*m,X0+10+118*m,Y0+51*m);

  Line(X0+10+62*m,Y0+27*m,X0+10+62*m,Y0+45*m);

  Line(X0+10+88*m,Y0+19*m,X0+10+88*m,Y0+45*m);

//Заполняем штриховкой

  SetBrushColor(clBlack);

  SetBrushStyle(bsHatch);

  SetBrushHatch(bhBackwardDiagonal);

  FillRect(X0+10+2,Y0+51*m+2,X0+10+118*m-1,Y0+63*m-1);

  FillRect(X0+10+2+62*m,Y0+63*m-1,X0+10+118*m-1,Y0+71*m-1);

  FillRect(X0+10+2+88*m,Y0+71*m-1,X0+10+118*m-1,Y0+90*m-1);

//Рисуем ось детали

  ...................................

end.


Примечание: за основу взят код из второго задания 20-ого параграфа. При этом код, который остался без изменения, заменён на строку с многоточием.


2.

unit MRisunki;

uses GraphABC;


var fi:real;//Угол поворота лопастей


procedure DrawDomik(X0,Y0:integer);//Рисует домик

begin

//Контур дома

  SetBrushColor(clBurlyWood);

  Rectangle(X0+5,Y0+15,X0+25,Y0+40);

//Крыша

  Line(X0+15,Y0,X0,Y0+15);

  Line(X0+15,Y0,X0+30,Y0+15);

  Line(X0,Y0+15,X0+30,Y0+15);

  FloodFill(X0+15,Y0+5,clYellowGreen);

  SetBrushColor(clBlack);

  Rectangle(X0+17,Y0+30,X0+22,Y0+40);//Дверь

  SetBrushColor(clBlue);

  Rectangle(X0+8,Y0+30,X0+13,Y0+35);//Окно нижнее

  Rectangle(X0+17,Y0+20,X0+22,Y0+25);//Окно верхнее левое

  Rectangle(X0+8,Y0+20,X0+13,Y0+25);//Окно верхнее правое

  Circle(X0+15,Y0+8,2);//Окно на крыше

end;


procedure DrawDerevo(X0,Y0:integer);//Рисует дерево

begin

//Ствол

  SetBrushColor(clBurlyWood);

  Rectangle(X0+13,Y0+30,X0+17,Y0+40);

//Крона

  SetBrushColor(clGreen);

  FillEllipse(X0+11,Y0,X0+19,Y0+7);

  FillEllipse(X0+7,Y0+5,X0+23,Y0+19);

  FillEllipse(X0+3,Y0+14,X0+27,Y0+32);

end;


procedure DrawMelnica(X0,Y0:integer);//Рисует мельницу

begin

  SetBrushColor(clBrown);

  FillCircle(X0+15,Y0+15,5);

  SetPenColor(clBrown);

  Line(X0+10,Y0+15,X0+5,Y0+40);

  Line(X0+20,Y0+15,X0+25,Y0+40);

  Line(X0+5,Y0+40,X0+25,Y0+40);

  FloodFill(X0+15,Y0+35,clBrown);

end;


procedure DrawLopasti(X0,Y0:integer);//Рисует лопасти

var x,y:integer;//Координаты центра лопастей

begin

  x:=X0+15; y:=Y0+15;

  SetPenColor(clBlack);

  Line(x,y,Round(x+Cos(fi)*14),Round(y-Sin(fi)*14));

  Line(x,y,Round(x+Cos(fi+Pi/2)*14),Round(y-Sin(fi+Pi/2)*14));

  Line(x,y,Round(x+Cos(fi+Pi)*14),Round(y-Sin(fi+Pi)*14));

  Line(x,y,Round(x+Cos(fi+3*Pi/2)*14),Round(y-Sin(fi+3*Pi/2)*14));

  fi:=fi+0.0628;

  if fi>(2*Pi) then fi:=fi-2*Pi;

end;


procedure Clear(x,y:integer);//Стирает область 30х40

begin

  SetPenColor(clLimeGreen);

  SetBrushColor(clLimeGreen);

  Rectangle(x,y,x+30,y+40);

end;


procedure DrawCar(X0,Y0:integer);

begin

//Стёкла

  SetBrushColor(clMediumTurquoise);

  FillRect(X0+5,Y0+12,X0+25,Y0+17);

  FillRect(X0+5,Y0+27,X0+25,Y0+32);

//Кузов

  SetBrushColor(clIndianRed);

  FillRect(X0+5,Y0+4,X0+25,Y0+12);

  FillRect(X0+5,Y0+17,X0+25,Y0+27);

  FillRect(X0+5,Y0+32,X0+25,Y0+37);

//Фары передние

  SetBrushColor(clBeige);

  FillRect(X0+7,Y0+2,X0+10,Y0+4);

  FillRect(X0+20,Y0+2,X0+23,Y0+4);

//Фары задние

  SetBrushColor(clRed);

  FillRect(X0+7,Y0+37,X0+10,Y0+38);

  FillRect(X0+20,Y0+37,X0+23,Y0+38);

//Колёса

  SetBrushColor(clBlack);

  FillRect(X0+3,Y0+10,X0+5,Y0+18);

  FillRect(X0+3,Y0+27,X0+5,Y0+35);

  FillRect(X0+25,Y0+10,X0+27,Y0+18);

  FillRect(X0+25,Y0+27,X0+27,Y0+35);

end;

end.


3.

program Risunki;

uses GraphABC,MRisunki;


type koordinati=record

     x,y:integer end;

var Tr:array [1..5] of koordinati;//Траектория движения


begin

//Рисуем фон и располагаем объекты по экрану

  FloodFill(1,1,clLimeGreen);

  DrawDomik(7,10);     DrawDerevo(66,15);    DrawDerevo(114,12);

  DrawDomik(31,66);    DrawDerevo(81,128);   DrawDerevo(16,155);

  DrawDerevo(11,274);  DrawDomik(69,312);    DrawDerevo(33,384);

  DrawDomik(102,416);  DrawDomik(237,375);   DrawDerevo(309,433);

  DrawDerevo(316,336); DrawDerevo(391,415);  DrawDerevo(259,249);

  DrawDomik(176,224);  DrawDerevo(219,165);  DrawDerevo(267,93);

  DrawDomik(319,177);  DrawDerevo(325,284);  DrawDomik(426,11);

  DrawDerevo(527,30);  DrawDerevo(445,82);   DrawDomik(544,97);

  DrawDerevo(590,143); DrawDerevo(485,129);  DrawDomik(527,238);

  DrawDerevo(600,331);


//Заполняем траекторию движения

  Tr[1].x:=200; Tr[1].y:=440;

  Tr[2].x:=90;  Tr[2].y:=210;

  Tr[3].x:=260; Tr[3].y:=20;

  Tr[4].x:=430; Tr[4].y:=210;

  Tr[5].x:=490; Tr[5].y:=440;

//Цикл анимации лопастей мельниц и движения автомобиля

  LockDrawing;

  for var i:=1 to 4 do

    begin

      var k:=(Tr[i+1].y-Tr[i].y)/(Tr[i+1].x-Tr[i].x);

      var b:=Tr[i].y-k*Tr[i].x;

      var j:=Tr[i].x;

      while j<>Tr[i+1].x do

        begin

          DrawMelnica(68,66);     DrawLopasti(68,66);

          DrawMelnica(178,296);   DrawLopasti(178,296);

          DrawMelnica(600,238);   DrawLopasti(600,238);

          DrawCar(j,Round(k*j+b));

          Redraw;

          Sleep(1);

          clear(68,66);

          clear(178,296);

          clear(600,238);

          clear(j,Round(k*j+b));

          if Tr[i+1].x>Tr[i].x then inc(j) else Dec(j);

        end;

    end;

  DrawCar(Tr[5].x,Tr[5].y);

//Цикл анимации лопастей мельниц 

  while true do

    begin

      DrawMelnica(68,66);     DrawLopasti(68,66);

      DrawMelnica(178,296);   DrawLopasti(178,296);

      DrawMelnica(600,238);   DrawLopasti(600,238);

      Redraw;

      Sleep(1);

      clear(68,66);

      clear(178,296);

      clear(600,238);

    end;

end.




Предыдущий параграф Назад в содержание Следующий параграф