Главная страница
Навигация по странице:

  • 8. Вывод текста с помощью библиотеки SDL_ttf

  • прогинг. Гущин_SDL. Аа нн


    Скачать 1.04 Mb.
    НазваниеАа нн
    Анкорпрогинг
    Дата13.10.2019
    Размер1.04 Mb.
    Формат файлаpdf
    Имя файлаГущин_SDL.pdf
    ТипУчебное пособие
    #89893
    страница5 из 7
    1   2   3   4   5   6   7
    7. Имитация движения при выводе на дисплей
    Имитация движения при выводе на дисплей (монитор) обусловливается инерционностью зрения человека: несколько неподвижных изображений некоторого объекта, на каждом из которых этот объект смещен относительно фона, при последо-вательном предъявлении с достаточно небольшими интервалами воспринимаются как перемещение данного объекта относительно фона.
    Такой же принцип имитации движения используется в кинематографии и телевидении.
    При программном построении движущихся по экрану мони-тора изображений может использоваться ряд способов, обеспе-чивающих видимое смещение движущихся объектов относительно фона и прочих неподвижных объектов.
    Самый простейший способ – рисование на мониторе всего изображения (кадра), задержка для его демонстрации, стирание и рисование следующего кадра с новым относительным располо- жением объектов.
    Достоинством способа можно считать возможность непосред-ственного отображения отдельно смоделированного простран-ственного положения подвижных и неподвижных объектов, отсутствие необходимости запоминать предшествующее изобра-жение на мониторе или его фрагменты, универсальность исполь-зования как при продолжении отображения движения, так и при полной смене изображения.
    Основным недостатком способа являются требования по быстродействию для обеспечения такой частоты смены кадров, которая бы не вызывала у человека ощущения мерцания экрана. В зависимости от разрешения и размера изображения приемлемой частотой смены кадров может

    39 быть частота от 25 до 100 кадров в секунду. При этом если изображение формируется непосред- ственно в видеопамяти (непосредственно рисуется на экране), а аппаратная частота обновления экрана, по данным видеопамяти, превосходит требуемую частоту кадров, возможна ситуация, когда видеоадаптер за расчетное время отображения одного кадра отобразит несколько кадров, на части которых изображение в кадре будет еще не дорисовано до конца. Чтобы избежать такой ситуации, изображение кадра должно быть сначала полностью построено в некоторой отдельной области памяти и только потом перенесено в видеопамять. Поскольку в библиотеке SDL исходно используется принудительное обновление отображаемой области по готовности изображения, то для устранения влияния недо-строенных кадров достаточно корректно расставить вызовы функ- ций обновления экрана и задержки для отображения. Приведем пример, показывающий реализацию способа с покадровой пере-рисовкой:
    SDL_Surface *scr; /* Основная поверхность отображения */ void Draw_Background(SDL_Surface *surf); /* Рисует на поверхности surf неподвижный фон, всегда одинаковый
    – реализация не приводится. При этом все предыдущее изображение удаляется */ void Draw_MovedObject(SDL_Surface *surf, Uint32 posnum);
    /* – Рисует на поверхности surf изображение некоторого движущегося объекта – один раз, в позиции номер posnum
    (пересчет номера позиции в конкретные координаты – зависит от конкретного изображения и не приводится, как и остальная реализация */ void Draw_Frame(SDL_Surface *surf, Uint32 framenum)
    /* – Рисует на поверхности surf один кадр из последовательности, показывающей движение объекта.
    Номер кадра передается вторым параметром framenum.
    Тело функции ниже: */
    {
    Draw_Background(surf); /* Фон одинаковый */
    Draw_MovedObject(surf, framenum); /* На каждом кадре – новая позиция объекта *
    }
    /* Инициализация библиотеки SDL и создание поверхности scr не приводятся */
    /* Цикл отображения ролика примерно на 1 минуту, всего
    60 секунд * 25 кадров в секунду = 1500 кадров */ int framecnt = 0;
    Uint32 before_next_frame = 40; /* 40 миллисекунд – задержка между перерисовкой кадров, если считать, что собственно рисование одного кадра ОЧЕНЬ быстрое */
    /* Первый кадр – без задержек: */
    Draw_Frame(sсr, framecnt++ ); /* Первый кадр */ while(framecnt < 1500)
    {
    SDL_Flip(scr); /* Отображение кадра на экране */
    SDL_Delay(before_next_frame);
    Draw_Frame(sсr, framecnt++ ); /* следующий кадр */
    }
    Альтернативой перерисовке всего кадра является перерисовка только его изменяющейся части, включающая два этапа: 1) восстановление фонового изображения на месте предыдущего изображения движущегося объекта; 2) рисование движущегося объекта в новом положении.
    Каждый из этапов, в зависимости от сложности изображения (фонового или накладываемого), имеющихся ресурсов и средств программирования отображения, может быть реализован либо с помощью собственно рисования (попиксельного, с помощью при-митивов и т.п.) участка фона или собственно движущегося изо-бражения, либо с помощью операций над областями изображений.
    В первом варианте проблемы возникают при достаточно слож-ном фоновом рисунке, если для него не удается создать отно-сительно простую функцию рисования фрагмента заданной фор-мы, находящегося в некоторой окрестности указанной точки. Для рисования самого движущегося

    40 объекта дополнительных проблем, по сравнению с покадровым рисованием, не возникает. В целом, по сравнению с покадровым рисованием, данный способ является более быстродействующим, особенно если изменяется небольшой фрагмент изображения по отношению к неизменному фону или имеется несколько таких небольших перемещающихся объектов, возможно, с различными законами движения, а рисование фраг-мента фона в заданных координатах не представляет трудности.
    Во втором случае идея рисования выглядит иначе. Фон является заданным и даже столь сложным, что его можно изобразить только целиком. Фрагмент фона в том месте, где будет выведен перемещающийся объект, копируется в некоторый буфер, затем происходит рисование кадра с текущим положением движу-щегося объекта прямо на исходном фоне, а перед рисованием следующего кадра сохраненный фрагмент фона восстанавливается на прежнем месте. Сам движущийся объект тоже может быть нари-сован однократно (если изменяется только его позиция) и на каж-дом кадре только накладываться в новом месте на фоне. Но для этого либо его форма должна совпадать с имеющимися возмож-ностями программных средств по копированию и наложению изо-бражений, либо потребуется явное указание, какие пиксели накла-дываемого изображения на самом деле прозрачны и не должны изменять состояние пикселей фона.
    Реализация движения с непосредственным частичным измене-нием изображения является очевидной, но сильно зависящей от конкретной задачи, поэтому отдельный пример рассматриваться не будет, а подобный способ построения движущихся изобра-жений будет приведен далее при рассмотрении обработки событий от клавиатуры.
    Рассмотрим пример демонстрации полета вертолета на фоне облачного неба. Само небо формируется заливкой голубым, с последующим нанесением в случайных местах белых и слегка сероватых эллипсов – облаков. Вертолет для имитации вращения винтов на разных кадрах рисуется с разным их положением. Создается четыре поверхности: одна – для отображения, вторая – для рисования вертолета (в разных положениях), третья – для однократного рисования на ней фона (без неѐ можно обойтись, рисуя фон непосредственно на поверхности для отображения) и четвертая – для временного сохранения фрагмента фона, затирае-мого вертолетом. Программа будет выглядеть следующим образом:
    #include
    #include
    #include
    #include const int scrwidth = 1027, scrheight = 768, scrdepth = 32; void draw_heli(SDL_Surface *surf, int centre_x, int centre_y, int phase_big, int phase_samll)
    { const int lw = 200, lh = 80; if(surf)
    { if(surf->w >= centre_x + lw/2 && surf->h >= centre_y + lh/2 && centre_x - lw/2 >= 0 && centre_y - lh/2 >= 0)
    {
    SDL_Rect dstarea; double phase;
    Sint16 x1, y1, x2, y2;
    Uint32 keycolor = SDL_MapRGB(surf->format, 0, 255, 0);
    Uint32 helicolor = SDL_MapRGB(surf->format,
    100, 100, 255);
    Uint32 phasecolor = SDL_MapRGB(surf->format,
    255, 100, 200); int i; dstarea.x = centre_x - lw/2; dstarea.y = centre_y - lh/2; dstarea.w = lw; dstarea.h = lh;
    SDL_FillRect(surf, &dstarea, keycolor);
    Draw_FillEllipse(surf, centre_x + 40, centre_y + 10,
    60, 30, helicolor);
    Draw_FillEllipse(surf, centre_x + 60, centre_y,

    41 25, 15, keycolor); dstarea.x = centre_x + 35; dstarea.y = centre_y - 30; dstarea.w = 10; dstarea.h = 15;
    SDL_FillRect(surf, &dstarea, helicolor); dstarea.x = centre_x - 80; dstarea.y = centre_y - 20; dstarea.w = 120; dstarea.h = 10;
    SDL_FillRect(surf, &dstarea, helicolor); dstarea.x = centre_x - 80; dstarea.y = centre_y - 30; dstarea.w = 10; dstarea.h = 30;
    SDL_FillRect(surf, &dstarea, helicolor); for(i = 0; i < 15; i++)
    { phase = M_PI / 180 * (phase_samll + i) ; x1 = floor(centre_x - 75 - (15 - i/2)*cos(phase)); y1 = floor(centre_y - 25 + (15 - i/2)*sin(phase)); x2 = floor(centre_x - 75 + (15 - i/2)*cos(phase)); y2 = floor(centre_y - 25 - (15 - i/2)*sin(phase));
    Draw_Line(surf, x1, y1, x2, y2, phasecolor);
    } for(i = 0; i < 15; i++)
    { phase = M_PI / 180 * (phase_big + i) ; x1 = centre_x + 40; y1 = centre_y - 30; x2 = floor(x1 + (60 - i/2)*cos(phase)); y2 = floor(y1 - (7 - i/2));
    Draw_Line(surf, x1, y1, x2, y2, phasecolor); x2 = floor(x1 - (60 - i/2)*cos(phase));
    Draw_Line(surf, x1, y1, x2, y2, phasecolor);
    }
    }
    }
    } void draw_sky(SDL_Surface *surf, int cloud_cnt)
    { int i;
    Sint16 x0, y0;
    Uint16 xr, yr;
    Uint8 cl;
    Draw_FillRect(surf, 0, 0, surf->w, surf->h,
    SDL_MapRGB(surf->format,0,200,255)); for(i = 0; i < cloud_cnt; i++)
    { xr = floor((rand()*0.05)/RAND_MAX * surf->w); yr = floor((rand()*0.05)/RAND_MAX * surf->h); x0 = xr + floor((rand()*1.0)/RAND_MAX *
    (surf->w - 2*xr)); y0 = yr + floor((rand()*1.0)/RAND_MAX *
    (surf->h - 2*yr)); cl = 220 + floor((rand()*1.0)/RAND_MAX * 35);
    Draw_FillEllipse(surf, x0, y0, xr, yr,
    SDL_MapRGB(surf->format, cl, cl, cl));
    }
    } int main ( int argc, char** argv )
    {

    42
    SDL_Surface *background, *temp, *sprites; int frame_num;
    SDL_Rect frame_src, frame_dst, frame_tmp; if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
    { printf( "Unable to init SDL: %s\n", SDL_GetError() ); return 1;
    }
    SDL_Surface* screen = SDL_SetVideoMode(scrwidth, scrheight, scrdepth, SDL_HWSURFACE|SDL_DOUBLEBUF); if ( !screen )
    { printf("Unable to set 640x480 video: %s\n",
    SDL_GetError()); return 1;
    } background = SDL_CreateRGBSurface(SDL_HWSURFACE |
    SDL_DOUBLEBUF, scrwidth, scrheight, scrdepth, screen->format->Rmask, screen->format->Gmask, screen->format->Bmask, screen->format->Amask); temp = SDL_CreateRGBSurface(SDL_HWSURFACE |
    SDL_DOUBLEBUF, 200, 80, scrdepth, screen->format->Rmask, screen->format->Gmask, screen->format->Bmask, screen->format->Amask); sprites = SDL_CreateRGBSurface(SDL_HWSURFACE |
    SDL_DOUBLEBUF, 200, 80*36, scrdepth, screen->format->Rmask, screen->format->Gmask, screen->format->Bmask, screen->format->Amask); if( !(background && temp && sprites) )
    { printf("Unable to create temporary surfaces: %s\n",
    SDL_GetError()); return 1;
    }
    SDL_SetColorKey(sprites, SDL_SRCCOLORKEY,
    SDL_MapRGB(sprites->format, 0, 255, 0)); for(frame_num = 0; frame_num < 36; frame_num ++) draw_heli(sprites, 100, 40+80*frame_num,
    10*frame_num, 10*frame_num); draw_sky(background, 250);
    SDL_BlitSurface(background, NULL, screen, NULL); int step = 0; while (step < (50*60)) /* Примерно 1 минута */
    { frame_tmp.x = 0; frame_tmp.y = 0; frame_tmp.w = 200; frame_tmp.h = 80; frame_dst.x = -200 + step % (scrwidth +200); frame_dst.y = scrheight/2 - 100; frame_dst.w = 200; frame_dst.h = 80;
    SDL_BlitSurface(screen, &frame_dst, temp, &frame_tmp); frame_src.x = 0; frame_src.y = 80*(step % 36); frame_src.w = 200; frame_src.h = 80;
    SDL_BlitSurface(sprites, &frame_src, screen, &frame_dst); step++;
    SDL_Flip(screen);
    SDL_Delay(20); /* 50 кадров/с */
    SDL_BlitSurface(temp, &frame_tmp, screen, &frame_dst);
    }
    SDL_Quit();

    43 return 0;
    }
    Конкретный способ или комбинация способов для имитации движения зависят от требований к производительности программы, от сложности ее разработки и сопровождения, а также от индиви- дуального управления отдельных перемещаемых по экрану объек-тов.
    8. Вывод текста с помощью библиотеки SDL_ttf
    Непосредственно в составе библиотеки SDL отсутствуют сред-ства вывода на графические поверхности текстовой информации, отличные от программного попиксельного отображения каждого символа. Отсутствуют они и в библиотеке SDL_draw. Основной проблемой для разработки таких средств в составе многоплат-форменных графических библиотек является обеспечение едино- образного отображения текста на различных целевых платформах, поскольку далеко не все они могут поддерживать одинаковые по своим возможностям средства отображения текста. Это приводит к необходимости обеспечивать единый интерфейс к множеству раз-личных реализаций средств отображения текста, что существенно усложнило бы библиотеку. Еще большей проблемой при этом является возможное отсутствие единообразия в представлении используемых шрифтов.
    Для устранения указанных проблем разработчиками библиотеки SDL создано расширение – библиотека SDL_ttf, версия которой 2.0.11 и будет рассмотрена далее.
    Библиотека SDL_ttf является интерфейсом-надстройкой над кроссплатформенной библиотекой FreeType 2.0, обеспечивающей единообразное построение в операционных системах семейств Linux, Mac OS X и Windows изображений символов по их двух-байтовым обозначениям согласно UNICODE, с использованием векторных шрифтов формата TrueType (.ttf), а также некоторых шрифтов формата .fon. При этом библиотека FreeType непосредственно не отвечает за перенесение построенных изображений на устройство отображения. Библиотека SDL_ttf обеспечивает формирование на основе текстовой строки специально создаваемой временной поверхности, содержащей изображение данного текста, выполненное указанным шрифтом с указанными параметрами. Собственно перенос на устройство отображения сводится к наложению созданной поверхности на непосредственно отображаемую поверхность. Также библиотека
    SDL_ttf содержит средства по преобразованию кодировок символов, определению размеров области, которую будет занимать конкретный текст при его отображении с указанными параметрами, средства выбора шрифтов и задания параметров и т.п. Более подробно с возможностями библиотеки SDL_ttf можно ознакомиться из комментариев в заголовочном файле
    SDL_ttf.h (на английском языке, непосредственно от разработчика SDL и SDL_ttf).
    Для возможности использования в программе функций SDL_ttf необходимо подключить заголовочный файл SDL_ttf.h:
    #include "SDL_ttf.h"
    Перед обращением к функциям из библиотеки ее необходимо проинициализировать с помощью функции TTF_Init: extern DECLSPEC int SDLCALL TTF_Init(void);
    При успешной инициализации TTF_Init возвращает 0, в случае ошибки –1, что может использоваться для определения возмож-ности продолжения программы. Соответственно для корректного освобождения ресурсов при завершении работы программы необходимо вызвать функцию деинициализации TTF_Quit: extern DECLSPEC void SDLCALL TTF_Quit(void);
    Для работы в программе с конкретным шрифтом конкретного размера необходимо по файлу с данным шрифтом создать необхо-димые структуры данных – «открыть шрифт». Для этого служит функция TTF_OpenFont: extern DECLSPEC TTF_Font * SDLCALL TTF_OpenFont(const char *file, int ptsize);

    44
    Первым параметром передается имя файла со шрифтом (либо полное имя, либо относительно текущего каталога, как правило – каталога с исполняемым файлом программы). Независимо от наличия пути к файлу в первом аргументе собственно имя файла не может как-либо сокращаться.
    Например, если расширение имени файла в данной операционной системе считается отдельным компонентом, а не частью имени после последней точки, оно также должно быть явно указано.
    Второй параметр – требуемый размер шрифта в пунктах (как в текстовых редакторах). Функция возвращает указатель на динамически размещаемую структуру типа TTF_Font (такой указатель при работе с библиотекой SDL_ttf часто также называют шрифтом по аналогии с указателями на файловый поток), содержащую информацию, необходимую для отображения символов данным шрифтом данного размера. При невозможности создания такой структуры (не найден файл шрифта, в нем отсутствует описание для запрашиваемого размера символов, закончилась память или при иных ошибках) функция возвращает NULL. Когда сведения о данном размере шрифта из указанного файла перестают быть необходимыми, нужно освободить память, выделенную под структуру TTF_font и свя-занные с ней другие структуры данных, для чего вызвать функцию
    TTF_CloseFont, передав ей в качестве аргумента указатель на структуру TTF_Font. Объявление функции TTF_CloseFont выгля-дит так: extern DECLSPEC void SDLCALL TTF_CloseFont(TTF_Font *font);
    Она корректно обрабатывает значение font, равное NULL, – просто ничего не делая.
    Кроме функции OpenFont, в некоторых случаях могут быть полезны функции
    TTF_OpenFontIndex,
    TTF_OpenFontRW,
    TTF_OpenFontIndexRW, различающиеся своими параметрами, более подходящими для специфических ситуаций. Рекомендуется самостоятельно изучить способы применения данных функций по комментариям в заголовочном файле SDL_ttf.h и исходному тексту определения данных функций – файлу SDL_ttf.c.
    Выполнение основной задачи библиотеки SDL_ttf, а именно отображение текста на поверхность, организовано с помощью ряда специальных функций, имена которых начинаются на
    TTF_RenderText_, TTF_RenderUTF8_, TTF_RenderUNICODE_, а последнее слово – Solid, Shaded или Blended. Для примера рассмотрим объявления функций TTF_RenderText_Solid,
    TTF_RenderUTF8_Solid, TTF_RenderUNICODE_Solid: extern DECLSPEC SDL_Surface * SDLCALL
    TTF_RenderText_Solid(TTF_Font *font, const char *text, SDL_Color fg); extern DECLSPEC SDL_Surface * SDLCALL
    TTF_RenderUTF8_Solid(TTF_Font *font, const char *text, SDL_Color fg); extern DECLSPEC SDL_Surface * SDLCALL
    TTF_RenderUNICODE_Solid(TTF_Font *font, const Uint16 *text, SDL_Color fg);
    Первый параметр всех трех функций – указатель на ранее успешно открытый шрифт требуемого размера, третий параметр – структура SDL_Color, в компонентах r, g и b которой описан тре-буемый цвет шрифта (значениями красной, зеленой и синей сос-тавляющей цвета).
    Второй параметр задает собственно отобра-жаемый текст в виде указателя на последовательность в памяти кодов отображаемых символов, заканчивающуюся символом с кодом 0. Функция
    TTF_RenderText_Solid предполагает, что коды символов соответствуют кодировке Latin-1 по принципу «один байт – один символ», функция TTF_RenderUTF8_Solid рассмат-ривает текст как закодированный согласно UTF8, где одному символу соответствует либо один байт, либо последовательность из двух и более байтов. Функция TTF_RenderUNICDOE_Solid предполагает, что все символы представлены непосредственно в кодировке UNICODE по принципу «один символ – одно 16-раз-рядное целое число без знака (двухбайтовое целое без знака)». Фактически два предыдущих варианта сводятся к использованию TTF_RenderUNICDOE_Solid после соответствующего перекоди-рования текста, однако для задания текстовых констант в теле программы чаще всего легче использовать UTF8 внутри обычных строковых констант языка C (не все компиляторы пока еще корректно поддерживают тип wchar и соответствующие строковые константы).

    45
    Все функции отображения текста возвращают указатель на созданную новую поверхность минимально необходимого размера, содержащую изображение текста или NULL в случае ошибки.
    Функции, заканчивающиеся на _Shaded и _Blended, отличаются наборами параметров и качеством отображения текста: оно выше, чем у функций с суффиксом _Solid. Соответственно изменяется и формат возвращаемой поверхности.
    Кроме функций отображения целой строки текста, в библиотеке существуют функции отображения отдельного символа, заданного своим кодом в кодировке UNICODE –
    TTF_RenderGlyph_Solid, TTF_RenderGlyph_Shaded и TTF_RenderGlyph_Blended. Подроб-ности о параметрах и возвращаемых результатах всех функций отображения текста можно узнать из комментариев в файле SDL_ttf.h.
    Во всех случаях функции отображения текста возвращают новую поверхность, причем имеющую такой формат, который однозначно указывает, где на ней текст, а какая часть поверхности должна рассматриваться как прозрачная. Эту поверхность для отображения на экране необходимо наложить с помощью функции SDL_BlitSurface на поверхность, полученную при инициализации видеорежима (или на какую-либо другую поверхность, исполь-зуемую в дальнейшем отображении).
    Тот факт, что функции семейства TTF_Render возвращают указатель на вновь создаваемую поверхность, позволяет увели-чивать быстродействие программы за счет увеличения расхода памяти: для многократно повторяющегося текста можно одно-кратно создать содержащую его поверхность, а затем многократно ее отображать, возможно, в разных точках и даже на разных целевых поверхностях. Однако это требует обязательного осво-бождения получаемых поверхностей, как только исчезает необхо-димость в их отдельном сохранении для предотвращения утечки ресурсов. Для этого служит функция SDL_FreeSurface основной библиотеки SDL: extern DECLSPEC void SDLCALL
    SDL_FreeSurface(SDL_Surface *surface);
    Таким образом, процесс вывода традиционного текста «При-вет, Мир!» с отступом от верхнего левого угла окна на 200 пик-селей по горизонтали и 100 пикселей по вертикали шрифтом типа TrueType, расположенным в файле 1.ttf в каталоге с исполняемым файлом программы, ярко- зеленым цветом размером 14 пунктов с максимальным быстродействием за счет уменьшения качества, будет выглядеть следующим образом (при условии, что среда раз-работки и компилятор поддерживают исходные тексты в кодиров-ке UTF8 и именно в ней был набран текст программы):
    /* Фрагмент, отвечающий за вывод текста. Переменная screen – поверхность, соответствующая окну программы.
    Ей присвоен результат функции
    SDL_SetVideoMode*/ if(screen){
    SDL_Color text_color;
    SDL_Rect dest;
    SDL_Surface *text_surface = NULL;
    TTF_Font * text_font = TTF_OpenFont("1.ttf", 14); if(text_font){ text_color.r = 0; text_color.g = 255; text_color.b = 0; dest.x = 200; dest.y = 100; text_surface = TTF_RenderUTF8_Solid(text_font,
    "Привет, Мир!", text_color); if(text_surface){
    SDL_BlitSurface(text_surface, NULL, screen, &dest);
    SDL_FreeSurface(text_surface);
    }
    TTF_CloseFont(fnt);
    }
    }
    В данном примере следует обратить внимание на проверку корректности выделения всех динамических ресурсов и их освобождение, а также на то, что после наложения поверхности с

    46 текстом на поверхность окна программы собственно поверхность с текстом можно удалить, а изображение в окне программы (точнее, на соответствующей окну поверхности) тем не менее останется до тех пор, пока не будет каким-либо образом замещено другим изображением или до завершения программы. Аналогично можно работать не только с текстовыми константами, но и с перемен-ными данными, располагающимися в некотором буфере – массиве или динамически выделяемой области памяти. При этом, как и при стандартной работе со строками, необходимо обеспечивать при заполнении такого буфера контроль выхода за его пределы и гарантировать наличие ограничивающего нулевого символа в конце строки внутри буфера. Дополнительно необходимо следить за соответствием типа данных в буфере используемой кодировки и соответствующей кодировке версии функции отображения текста. Пример программы с использованием нескольких шрифтов и одного шрифта в нескольких размерах и стилях приведен в приложении 2.
    1   2   3   4   5   6   7


    написать администратору сайта