12. Графика в Турбо Си

 

Вообще говоря, этот раздел можно было бы и вовсе опустить, ограничившись одним-единственным предложением: графические средства BGI в Турбо Си (здесь мы "привязываемся" именно к этой системе) полностью совпадают с графическими средствами BGI в Турбо Паскале (собственно, это вообще одно и то же — BGI един для этих двух систем программирования). Так что у тех, кто использовал графику BGI в Турбо Паскале, не бывает никаких сложностей при переходе на Си. Но поскольку все же хочется привести пример содержательной графической задачки, рассмотрим программу построения кривой, которая вычерчивается посредством так называемого "спирографа".

Игрушка "спирограф" многим знакома с детства. Спирограф представляет собой зубчатый диск радиуса Б, расположенный внутри кольца радиуса А (на внутренней границе кольца также имеются зубцы). Диск вращается против часовой стрелки (можно, разумеется, вращать и по часовой) и постоянно находится в зацеплении с внешним кольцом. В диске имеется небольшое отверстие на расстоянии D от центра, в которое вставляется грифель карандаша. В процессе вращения диска грифель вычерчивает рисунок, процесс заканчивается, когда конец карандаша возвращается в исходную точку.

Несколько лет назад автор этих строк готовил задания для проведения практикума по компьютерной графике. Задача написать программу, моделирующую спирограф, не вызывала проблем, тем более что она имеется в "зеленом" задачнике (этот задачник хорошо знаком читателям "Информатики"), В этом задачнике на с. 173 есть задача 853, в которой, помимо прочего, приводятся и параметрические формулы кривой, которую вычерчивает спирограф. Вот они:

х = (А - B)cos t- + Dcos a
у = (А - B)sin t - Dsin a

Здесь a = (А/В)/t,  D <B < А. Угол t меняется от 0 до 2*ПИ* n, а n = В/НОД(А, В).

Проблемы начались на следующий день, когда задания были выданы, выполнены и пришел черед проверять их. И тут я чуть не опростоволосился. Первый тестовый пример, который я ввел, был предельно простым: А = 200, В == 100, D = 100. Увидев изображение, приведенное на рисунке, очень похожее на то, что обычно получается с помощью спирографа, я уже было собрался перейти к следующему тесту, если бы не категоричное заявление моего ученика, который ждал своей очереди "проверяться". "Ерунда!" — заявил он, и был прав! В тот же самый момент я и сам понял, что это ерунда. Такого не могло быть. Рисунок, который должен получаться для этих исходных данных, существенно менее красив, зато значительно более правилен, это просто отрезок — диаметр большой окружности.

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

Рассмотрим правильную модель спирографа. Поместим центр большого кольца в начало координат, это несколько упростит рассуждения.

Повернем внутренний диск из начального положения (в начальном положении диск касается кольца в точке Р) на угол t. При этом центр диска перейдет из точки С в точку С, точка касания из точки Р перейдет в точку Р1, а точка D перейдет в точку D1. Так как диск катится без проскальзывания, длина дуги PQ равна длине  дуги QP1. Таким образом, At = Bt1, откуда t1= (А/В)t. Найдем координаты  точки D1. Координаты точки С1 находятся следующим образом:

Хc1= (A- B)cos -t  = (А - B) cos t
Yc1= (A- B)sin -t  = - (A- B) sin t

Точка D1 получается поворотом точки D' вокруг точки C1 на угол (t1 - t)

ХD1 = Хc1 + Dcos(t1 -t) = (А - B)cos t + Dcos((A/B)t - t)
YD1 = Yc1 + Dsin(t1 - t) = - (A- B)sin t + Dsin((A/B)t -t)

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

#include <stdio.h>
#include <conio.h>
#include <graphics.h>
#include <math.h>
void main(void)
  { int a,b,d,gd,gm;
      float xd,yd,t;
      printf("Радиус большой окружности?");scanf("%d",&a) ;
      printf("Радиус малой окружности?");scanf("%d",&b);
      printf("Расстояние от точки внутренней окружности до ее центра");scanf("%d",&d) ;
      t=0;
      gd=DETECT;
      initgraph(&gd,&gm,"с:\\tc") ;
      moveto(320+a-b+d,240) ;
      while (!kbhit())
        { xd=(a-b)*cos(t)+d*cos(a/b*t-t);
           yd=-(a-b)*sin(t)+d*sin(a/b*t-t);
           t=t+M_PI/180; /* Угол поворота - один градус */
           lineto (320+xd, 240-yd) ;
         }
    }

Можно также рассмотреть случай, когда диск катится по внешнему радиусу кольца.

Повернем внешний диск из начального положения на угол t. При этом, как и в рассмотренном выше случае, центр диска перейдет из точки С в точку С1 , точка касания из точки Р перейдет в  точку Р1 , а точка D перейдет в точку D1 . Длина дуги PQ равна длине  дуги QP1. Таким образом, по-прежнему At= Вt1, откуда t1= (А/В)t. Найдем координаты точки D1. Координаты точки С1 находятся следующим образом:

Xc1 = (А + В) cos t
Yc1 = (A+ В) sin t

Точка D1 получается поворотом точки D' вокруг точки С1 на угол (t1 — ПИ + t).

ХD1= Хc1+ Dcos(t1- t) = (А + B)cos t + Dcos((A/B)t- ПИ + t) = (А + B)cos t - Dcos((A/B)t - t)
YD1== Уc1+ Dsin(t1- t) = (A + B)sin t + Dsin((A/B)t - ПИ + t) = (A + B)sin t - Dsin((A/В)t + t)

Для полноты картины приведем программу, моделирующую "внешний" спирограф.

#include <stdio.h>
#include <conio.h>
#include <graphics.h>
#include <math.h>
void main(void) 
   { int a,b,d,gd,gm;
      float xd,yd,t;
      printf("Радиус внутренней окружности?");scant("%d",&a);
      printf("Радиус внешней окружности?");scanf("%d",&b);
      printf("Расстояние от точки внешней окружности до ее центра");scanf("%d",&d);
      t=0;
      gd=DETECT;
      initgraph(&gd,&gm,"c:\\tc");
      moveto(320+a-b+d,240) ;
      while (!kbhit())
         { xd=(a+b)*cos(t)-d*cos(a/b*t+t) ;
            yd=(a+b)*sin(t)-d*sin(a/b*t+t) ;
            t=t+M_PI/180; /* Угол поворота - один градус */
            lineto(320+xd,240-yd) ;
     }
 }

Обратите внимание, что прототипы функций для выполнения графических построений находится в файле graphics.h. Кроме того, первые два параметра функции initgraph являются указателями и их необходимо передавать в нее соответственно. Ну и, наконец, в строке, указывающей путь к графическому драйверу (равно как и во всех других строках), символ " \ " необходимо удваивать.

Выше при рассмотрении файлов проекта мы написали небольшую библиотеку функций для работы в 256-цветном режиме. В ней имелись функции для работы с палитрой и установки точки заданным цветом. Если еще уметь рисовать линии и окружности (причем не просто рисовать, а быстро и эффективно), можно расширить нашу библиотеку и иметь минимальный, но вполне достаточный набор графических функций для режима VGA 13h. Приведем функции рисования линии и окружности с использованием алгоритмов Брезенхема. Мы написали их для обычной BGI-графики, но вам (и детям) не составит никакого труда дополнить ими библиотеку vga256. К сожалению, подробное рассмотрение алгоритмов Брезенхема не укладывается в рамки нашего краткого курса. Заинтересованному читателю мы можем порекомендовать соответствующую статью (см.: "Информатика", № 32/96). Тем, кто не хочет разбираться в тонкостях данных алгоритмов, а готов удовлетвориться тем, что они действительно работают, поясним, в чем, собственно, их "соль": в них для построения линий и окружностей используется исключительно целочисленная арифметика. Это и делает алгоритмы Брезенхема столь эффективными (кстати, в несколько модифицированном виде эти же алгоритмы реализованы и в BGI).

Построение линии с использованием алгоритма Брезенхема

#include <stdio.h>
#include <stdlib.h>
#include <graphics.h>
#include <conio.h>
void bres_line(int xl,int yl,int x2,int y2,int color)
  { int gd,gm,i,x,y,dx,dy,ix,iy,increment,plotx,ploty,plot;
     dx=x2-xl;
     dy=y2-yl;
      ix=abs(dx);
      iy=abs(dy) ;
      if (dx>dy) increment=dx;else increment=dy;
      plotx=xl;
      ploty=yl;
      x=0;
      y=0;
      putpixel(plotx,ploty,color) ;
      for (i=0;i<=abs(increment);i++)
        { x+=ix;
           y+=iy;
           plot=0;
               if (x>increment)
                 { plot=l;
                    x-=increment;
                     if (dx>0) plotx++; else plotx--;
                 }
           if (y>increment)
             { plot=l;
                y-=increment;
                if (dy>0) ploty++; else ploty++;
              }
            if (plot) putpixel(plotx,ploty,color) ;
          }
   }
void main(void)
   { int xl,yl,x2,y2,gd=DETECT,gm,r;
      printf("xl=");scanf("%d",&xl) ;
      printf("yl=");scanf("%d",&yl) ;
      printf("x2=");scant("%d",&x2) ;
      printf("y2=");scanf("%d", &y2) ;
      initgraph(&gd,&gm,"C:\\TC");
      bres_line(xl,yl,x2,y2,WHITE) ;
      getch();
      closegraph ();
  }

Построение окружности с использованием алгоритма Брезенхема

#include <stdio.h>
#include <graphics.h>
#include <conio.h>
void sim(int xc,int yc,int x,int y,int color)
  { putpixel(x+xc,y+yc,color);
     putpixel(x+xc,-y+yc,color) ;
     putpixel(-x+xc,-y+yc,color) ;
     putpixel(-x+xc,y+yc,color);
     putpixel(y+xc,x+yc,color) ;
     putpixel(y+xc,-x+yc,color) ;
     putpixel("y+xc,-x+yc,color) ;
     putpixel(-y+xc,x+yc,color);
  }

void bres_circle(int xc,int yc,int r,int color)
  { int x,y,d;
     d=3-2*r;
     x=0;
     y=r;
     while (x<=y)
     { sim(xc,yc,x,y,color);
        if (d<0) d=d+4*x+6;
        else {d=d+4*(x-y)+10; y--;}
         x++;
     }
  }

void main(void)
  { int x,y,r,gd=DETECT,gm;
     printf("x=");scanf("%d",&x) ;
     printf("y=");scanf("%d",&y) ;
     printf("r=");scanf("%d",&r) ;
     initgraph(&gd,&gm,"C:\\TC") ;
     bres_circle(x,y,r,WHITE) ;
     getch() ;
     ciosegraph() ;
  } TopList