Думаю, многие видели красивую "листалку" обложек, используемую в продукции Apple. Да и не только у них. Для тех кто все-таки не слышал о таком - есть Википедия. К чему я завел этот разговор о Cover Flow? А к тому, что сегодня я расскажу вам как самому сделать нечто очень похожее на Cover Flow. Результат вы можете лицезреть на картинке выше. Если я заинтересовал вас, то добро пожаловать под кат.
Вступление
Какими средствами мы будем все это реализовывать? Начнём с того, что программа наша будет кроссплатформенная. Для работы с графикой будем использовать OpenGL и библиотеку GLUT. OpenGL есть в Windows, Mac OS X и Linux сразу. GLUT из коробки оказался только в Mac OS X. Для Windows можно взять здесь. Для Linux устанавливаем Freeglut (в Ubuntu доступен из репозиториев, в других дистрибутивах наверняка тоже). Писать будем на C. Ибо ничего не может быть лучше старого-доброго C. Для пущей кроссплатформенности фишки C99 использоваться не будут, т.к. их не поддерживает VC. И последнее. Для работы с каталогами я пользовался функциями из никсового dirent.h. В VS2010 такого заголовочного файла нету. Но зато есть его свободная реализация. Взять можно здесь. Скачанный заголовочный файл ложить в %Путь к Visual Studio%\VC\include\. Теперь можно приступать.Вспомогательные модули
Для начала напишем пару небольших вспомогательных модулей. Модуль для работы с каталогом (для загрузки всех изображений из указанного каталога), модуль для загрузки изображений в формате BMP (можно воспользоваться готовыми библиотеками, но это не для истинных джедаев, да и условия работы изначально предполагали ручную загрузку) и модуль для вывода строк на экран (да, в OpenGL цивилизованный вывод текста не предусмотрен).font.h
Начнём с самого простого. Тут будет всего одна функция. Но зато с наворотом в виде поддержки форматной строки.font.h
#ifndef _FONT_H_ #define _FONT_H_ #include <stdio.h> #include <stdarg.h> #if defined(__linux) || defined(_WIN32) #include <GL/glut.h> #else #include <GLUT/glut.h> #endif #define MAX_LENGTH 256 /*Max length of string*/ /*Draw string using OpenGL render. Supports format string*/ void glPrintString(float, float, float, short, short, const char*, ...); #endif
font.c
#include "font.h" //x, y - координаты, size - размер, - bold - жирность вкл/выкл, mono - моноширинный шрифт вкл/выкл, str - собственно строка void glPrintString(float x, float y, float size, short bold, short mono, const char* str, ...) { char* p; char text[MAX_LENGTH]; va_list ap; //Преобразуем форматную строку в обычную va_start(ap, str); vsprintf(text, str, ap); va_end(ap); //Начинаем рисовать буквы, начиная с координаты (x, y) glPushMatrix(); glTranslatef(x, y, 0.0f); //и масштабируем их в соответствии с размером size glScalef(0.01f * size, 0.01f * size, 1.0f); //Если включена жирность, то меняем толщину линии (OpenGL рисует буквы линиями) if(bold) glLineWidth(2 * size); //Собственно отрисовка текста по буквам обычным или моноширинным шрифтом for (p = text; *p; p++) { if(mono) glutStrokeCharacter(GLUT_STROKE_MONO_ROMAN, *p); else glutStrokeCharacter(GLUT_STROKE_ROMAN, *p); } glPopMatrix(); }
image.h
Напишем собственноручно загрузку изображений в форате BMP. Это очень просто, тем более, что мы будет загружать лишь 24-битные изображения без сжатия. Таких подавляющее большинство.image.h
#ifndef _IMAGE_H_ #define _IMAGE_H_ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #if defined(__linux) || defined(_WIN32) #include <GL/glut.h> #else #include <GLUT/GLUT.h> #endif unsigned int LoadTextureFromBitmap(char*); /*Works only with 24-bit bitmap images w/o RLE-compression*/ #endif
image.c
#include "image.h" unsigned int LoadTextureFromBitmap(char* file) { unsigned int Texture; FILE* img = NULL; int bWidth = 0; int bHeight = 0; unsigned int size = 0; unsigned char* data; //Читаем файл img = fopen(file,"rb"); if (img == NULL) { fprintf(stderr, "Failed to open file: %s: %s\n", file, strerror(errno)); return 0; } //Загружать весь заголовок в структуру мы не будем //Нам нужны лишь размеры изображения и собственно изображение //Читаем ширину и высоту изображения fseek(img, 18, SEEK_SET); fread(&bWidth, 4, 1, img); fread(&bHeight, 4, 1, img); if (bWidth == 0 || bHeight == 0) { fprintf(stderr, "Failed to read image size: %s: %s\n", file, strerror(errno)); return 0; } //Получаем размер изображения (без заголовков) fseek(img, 0, SEEK_END); size = ftell(img) - 54; //Выделяем память data = (unsigned char*)calloc(size, sizeof(unsigned char*)); //Читаем изображение fseek(img, 54, SEEK_SET); fread(data, size, 1, img); if (data == NULL) { fprintf(stderr, "Failed to read image data: %s: %s\n", file, strerror(errno)); return 0; } fclose(img); //Создаем текстуру //OpenGL каждой текстуре присваивает уникальный номер //Затем мы сможем вызывать нужную текстуру по этому номеру glGenTextures(1, &Texture); glBindTexture(GL_TEXTURE_2D, Texture); //Заголовочные файлы, поставляемые вместе с VS, находятся на уровне OpenGL 1.1 //Поэтому в Windows нужно использовать устаревшую константу GL_BGR_EXT вместо GL_BGR //Сама константа обозначает, что в Bitmap-файле порядок цветовых компонент обратный, т.е. Blue Green Red #ifdef _WIN32 gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB, bWidth, bHeight, GL_BGR_EXT, GL_UNSIGNED_BYTE, data); #else gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB, bWidth, bHeight, GL_BGR, GL_UNSIGNED_BYTE, data); #endif //Следующие строки указывают использовать линейную интерполяцию при масштабировании текстуры glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); free(data); return Texture; }
filemanager.h
Модуль специально написанный для облагораживания программы к этой статье. Изначально загружался текстовый файл со списком путей к изображениям. Но теперь все как у цивилизованных людей - указывается папка и из неё выбираются все файлы с расширением bmp. В нагрузку к этому ещё три функции: функция, выделяющая имя файла из полного пути; функция, выделяющая расширение файла из полного пути или имени файла; функция, склеивающая две строки.filemanager.h
#ifndef _FILEMANAGER_H_ #define _FILEMANAGER_H_ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #include <errno.h> #if defined(__linux) || defined(_WIN32) #include <GL/glut.h> #else #include <GLUT/GLUT.h> #endif char** LoadCoversList(char*, int*); /*Load covers list from dir*/ char** LoadCoversListFromFile(char*, int*); /*Load covers list from file*/ char* StripFilename(const char*); /*Return filename w/o path*/ char* StripFileType(const char*); /*Return filetype*/ char* JoinFilePath(const char*, const char*); /*Join dir and file path*/ #endif
filemanager.c
#include "filemanager.h" char** LoadCoversList(char* dirPath, int* count) { DIR* dir; struct dirent* dent; char** files = NULL; int i = 0; printf("Func: %s Dir: %s\n", __FUNCTION__, dirPath); //Открываем папку dir = opendir(dirPath); if(dir == NULL) { fprintf(stderr, "Failed to open current dir: %s\n", strerror(errno)); return NULL; } //Читаем содержимое каталога while(dent = readdir(dir)) { //Пропускаем все кроме файлов с расширением bmp if(dent->d_type == DT_REG && strcmp(StripFileType(dent->d_name), "bmp") == 0) { //Реаллочим массив строк files = (char**)realloc(files, (i + 1) * sizeof(char*)); if(files == NULL) { fprintf(stderr, "Failed to reallocate memory: %s\n", strerror(errno)); return NULL; } //Копируем строку в массив files[i] = (char*)strdup(dent->d_name); if(files[i] == NULL) { fprintf(stderr, "Failed to copy string: %s\n", strerror(errno)); return NULL; } i++; } } closedir(dir); *count = i; return files; } //Функция-анахронизм, в программе уже не используется char** LoadCoversListFromFile(char* filePath, int* count) { //Её содержимое опустим для краткости } //Возвращает имя файла (без пути) char* StripFilename(const char* str) { printf("Func: %s STR: %s\n", __FUNCTION__, str); #if defined(_WIN32) if(strrchr(str, '+') != NULL) return strdup(strrchr(str, '+') + 1); else return str; #else if(strrchr(str, '/') != NULL) return strdup(strrchr(str, '/') + 1); else return str; #endif } //Возвращает расширение файла char* StripFileType(const char* str) { printf("Func: %s STR: %s\n", __FUNCTION__, str); return strdup(strrchr(str, '.') + 1); } //Склеивает две строки в одну (в нашем случае путь к файлу и имя файла) char* JoinFilePath(const char* dir, const char* file) { //Получаем длину новой строки int len = strlen(dir) + strlen(file) - 1; //Выделяем память char* path = (char*)calloc(len, sizeof(char)); printf("Func: %s Dir: %s File: %s\n", __FUNCTION__, dir, file); //Копируем первую строку strcpy(path, dir); //Копируем вторую строку strcpy(path + strlen(dir), file); return path; }
cover.h
Самый важный модуль. Использует в работе три предыдущих. 13 параметров вынесены в define. Объявлен enum для удобства указания направления движения изображений. И конечно же структура. Исчерпывающие комментарии по структуре и функциям даны в коде.cover.h
#ifndef _COVER_H_ #define _COVER_H_ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include "image.h" #include "filemanager.h" #include "font.h" #if defined(__linux) || defined(_WIN32) #include <GL/glut.h> #else #include <GLUT/glut.h> #endif #define W_WIDTH 1000 /*Ширина окна*/ #define W_HEIGHT 400 /*Высота окна*/ #define X -27 /*Координата X точки начала отрисовки*/ #define Y 6 /*Координата Y точки начала отрисовки*/ #define COVERS 11 /*Количество обложек, отображаемых на экране*/ #define OFFSET 3 /*Смещение координат обложек*/ #define TXT_OFFSET 2.5 /*Отступ текста от верхней границы обложки*/ #define TXT_SIZE 1 /*Размер текста*/ #define WIDTH 12 /*Размер обложки*/ #define ANGLE 80 /*Угол поворота обложек, стоящих боком*/ #define TRANSPARENT 0.4f /*Коэффициент прозрачности отражений обложек*/ #define ANIM_TIME 250 /*Время анимации (в мс)*/ #define ANIM_STEP 16 /*Количество шагов анимации*/ typedef enum { LEFT, RIGHT } Direction; typedef struct { float angle; /*Угол поворота*/ short reflection; /*Отражение вкл/выкл*/ float transparent; /*Коэффициент прозрачности отражения обложки*/ unsigned int texture; /*Номер текстуры*/ char* name; /*Файл*/ short sign; /*Показывать имя файла вкл/выкл*/ float position; /*Порядковый номер обложки в "потоке"*/ float offset; /*Смещение обложки относительно положения по умолчанию*/ } Cover; void InitCoversArray(); /*Загружает массив обложек*/ void DrawAnimatedCovers(int); /*Передвигает обложки с плавной анимацией*/ void DrawCovers(); /*Рисует обложки*/ float CalculateX(float, float); /*Подсчитывает текущую координату Х для обложки*/ void MoveCovers(Direction); /*Передвигает обложку*/ void prints(); /*Служебная функция для распечатки структуры Cover в консоль*/ void ClearCovers(); /*Освобождает память занимаемую обложками*/ #endif
cover.c
#include "cover.h" //Путь к папке с обложками char dirPath[] = "./covers/"; //Массив обложек Cover* covers = NULL; int i, middle, count = 0; Direction move = LEFT; //Блокировка одновременного движения в противоположную сторону short leftLocked = 0, rightLocked = 0; float nextPos; void InitCoversArray() { //Загружаем список изображений char** coverList = LoadCoversList(dirPath, &count); //Выделяем память covers = (Cover*)calloc(count, sizeof(Cover)); //Считаем номер центральной обложки на экране middle = (COVERS + 1) >> 1; printf("Func: %s\n", __FUNCTION__); for(i = 0; i < count; i++) { //Инициализируем обложки Cover c = { (i + 1) <= middle ? ((i + 1) != middle ? ANGLE : 0.0f) : -ANGLE, /*Выбираем угол: обложки слева под углом ANGLE, центральная - 0, обложки справа под углом -ANGLE*/ 1, TRANSPARENT, LoadTextureFromBitmap(JoinFilePath(dirPath, coverList[i])), /*Загружаем изображение из файла, сохраняем номер текстуры*/ StripFilename(coverList[i]), /*Получаем имя файла для отображения*/ (i + 1) == middle ? 1 : 0, /*Включаем отображения названия только для центральной обложки*/ (float)(i + 1), (i + 1) < middle ? 0.0f : ((i + 1) != middle ? 4.0f : 2.0f) /*Устанавливаем персональное смещение для обложек (обложки слева - 0 центральная - 2 обложки справа - 4)*/ }; covers[i] = c; } } void DrawAnimatedCovers(int p) { //Статический счетчик шагов анимации static int counter = 0; //Флаг остановки static short stop = 0; printf("Func: %s\n", __FUNCTION__); //Если движемся влево, то блокируем на это время движение вправо. Если вправо, то блокируем движение влево. if(p == LEFT) rightLocked = 1; else if(p == RIGHT) leftLocked = 1; //Если анимация закончилась, то обнуляем счетчик if(stop) { counter = 0; stop = 0; } //Двигаем обложки в нужном направлении MoveCovers(p); counter++; //Запускаем таймер, который вызывает этот метод для осуществления анимации if(counter < ANIM_STEP) glutTimerFunc(ANIM_TIME / ANIM_STEP, DrawAnimatedCovers, p); else { stop = 1; rightLocked = 0; leftLocked = 0; } glutPostRedisplay(); } void DrawCovers() { for(i = 0; i < count; i++) { Cover c = covers[i]; if(c.position <= COVERS) { //Текущщий Х float x = CalculateX(c.position, c.offset); //Середина картинки float mid = x + (WIDTH >> 1); //Смещение от середины для центровки текста float center = mid - ((strlen(c.name) >> 1) * TXT_SIZE); glPushMatrix(); //Накладываем текстуру glBindTexture(GL_TEXTURE_2D, c.texture); //Передвигаем обложку в начало координат //Это нужно для того, чтобы поворачивать обложку вокруг собственной оси //Т.к. в OpenGL все вращения происходят на самом деле вокруг центральной оси glTranslatef(mid, 0, 0); //Поворачиваем обложку (если нужно) glRotatef(c.angle, 0, 1, 0); //Возвращаем обложку обратно glTranslatef(-mid, 0, 0); //Начинаем рисовать примитив четырёхугольника glBegin(GL_QUADS); glColor4f(1.0, 1.0, 1.0, 1.0); //Устанавливаем текстурные координаты (привязка углов текстуры к углам четырёхугольника) glTexCoord2f(0, 1); glVertex3i(x, Y, 0); glTexCoord2f(0, 0); glVertex3i(x, Y - WIDTH, 0); glTexCoord2f(1, 0); glVertex3i(x + WIDTH, Y - WIDTH, 0); glTexCoord2f(1, 1); glVertex3i(x + WIDTH, Y, 0); glEnd(); //Если включено отражение, рисуем четырехугольник с перевернутой текстурой и полупрозрачной верхней половиной if(c.reflection) { glBegin(GL_QUADS); glColor4f(1.0, 1.0, 1.0, c.transparent); glTexCoord2f(0, 0); glVertex3i(x, Y - WIDTH, 0); glTexCoord2f(1, 0); glVertex3i(x + WIDTH, Y - WIDTH, 0); //Следующие две вершины неведимые (полностью прозрачные). Благодаря этому получается эффект отражения glColor4f(1.0, 1.0, 1.0, 0.0); glTexCoord2f(1, 1); glVertex3i(x + WIDTH, Y - (WIDTH << 1), 0); glTexCoord2f(0, 1); glVertex3i(x, Y - (WIDTH << 1), 0); glEnd(); } //Выводим название обложки, если эта опция включена if(c.sign) { //Отключаем текстуру (иначе надпись отобразится неправильно, т.к. буквы нарисованы линиями и OpenGL попытается "натянуть" на них текстуру) glBindTexture(GL_TEXTURE_2D, 0); glColor4f(1.0, 1.0, 1.0, 1.0); //Для вывода текста используем написанную нами ранее функцию glPrintString(center, Y + (TXT_OFFSET * TXT_SIZE), TXT_SIZE, 1, 1, c.name); } glPopMatrix(); } } } float CalculateX(float pos, float offset) { //Считает текущую координату как смещение от стартовой координаты, используя позицию обложки, её персональное смещение и общее смещение return X + ((pos + offset - 1) * OFFSET); } void MoveCovers(Direction d) { move = d; //Передвигает обложки с плавной анимацией //Пошагово сдвигает позицию каждой обложки, выполняет поворот для центральной и следующей за ней обложки //Обеспечивает цикличность движения обложек if(d == RIGHT) { for(i = 0; i < count; i++) { covers[i].position += 1.0f / ANIM_STEP; covers[i].sign = covers[i].position == middle ? 1 : 0; nextPos = ceilf(covers[i].position); if(nextPos == middle || nextPos == middle + 1) { covers[i].angle -= ANGLE / ANIM_STEP; covers[i].offset += 2.0f / ANIM_STEP; } if(covers[i].position == nextPos && nextPos == count + 1) { covers[i].position = 1.0f; covers[i].angle = ANGLE; covers[i].offset = 0.0f; } } } else if(d == LEFT) { for(i = 0; i < count; i++) { covers[i].position -= 1.0f / ANIM_STEP; covers[i].sign = covers[i].position == middle ? 1 : 0; nextPos = floorf(covers[i].position); if(nextPos == middle || nextPos == middle - 1) { covers[i].angle += ANGLE / ANIM_STEP; covers[i].offset -= 2.0f / ANIM_STEP; } if(covers[i].position == nextPos && nextPos == 0) { covers[i].position = count; covers[i].angle = -ANGLE; covers[i].offset = 4.0f; } } } } void prints() { for(i = 0; i < count; i++) { printf("Cover #%d\n", i + 1); printf("----------------------------\n"); printf("Position = %f\n", covers[i].position); printf("Angle = %f\n", covers[i].angle); covers[i].reflection ? printf("Transparent coeff = %f\n", covers[i].transparent) : 0; printf("Name = %s\n", covers[i].name); printf("----------------------------\n"); } } void ClearCovers() { printf("Func: %s\n", __FUNCTION__); for(i = 0; i < count; i++) free(covers[i].name); free(covers); }
Последний шаг
Осталось последнее усилие. Написать инициализацию окна OpenGL, отклик на клавиатуру и мышь, функцию перерисовки окна и немного настроек OpenGL.main.c
#include "cover.h" extern short leftLocked, rightLocked; //Собственная функция инициализации void init() { //Настройки света float light_position[] = { 0.0, 1.0, 1.0, 0.0 }; float light_ambient[] = { 0.2, 0.2, 0.2, 1.0 }; glLightfv (GL_LIGHT0, GL_POSITION, light_position); glLightfv (GL_LIGHT0, GL_AMBIENT, light_ambient); //Антиалиасинг (рекомендуем OpenGL сглаживать линии, полигоны и устранять неровности перспективной проекции) glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //Сглаживание линий glEnable(GL_LINE_SMOOTH); //Включаем буфер глубины glClearDepth(1.0f); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); //Включаем освещение glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_COLOR_MATERIAL); //Включаем альфа-смешивание (прозрачность) glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); //Включаем текстуры glEnable(GL_TEXTURE_2D); //Инициализируем массив обложек InitCoversArray(); } //Функция, вызываемая при изменении размеров окна void reshape(int w, int h) { //Устанавливаем рабочую область размером во всё окно glViewport(0, 0, w, h); //Устанавливаем перспективную проекцию с углом обзора в 45 градусов glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0, (GLdouble)w/(GLdouble)h, 0.1, 150); //Устанавливаем модельно-видовую матрицу glMatrixMode(GL_MODELVIEW); glLoadIdentity(); //Устанавливаем камеру (первые три числа), точку куда смотрим (вторая тройка чисел) и нормаль (последние три числа) gluLookAt(0.0, 0.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); } //Функция, вызываемая при перерисовке экрана void display() { //Очищаем буфер цвета и буфер глубины glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //Вызываем нашу функцию рисования обложек DrawCovers(); //Меняем экранные буферы местами (у нас включена двойная буферизация) glutSwapBuffers(); } void keyboardSpec(int key, int x, int y) { //Вызывает функцию движения обложек по нажатию клавиш switch(key) { case GLUT_KEY_LEFT: if(!leftLocked) DrawAnimatedCovers(LEFT); break; case GLUT_KEY_RIGHT: if(!rightLocked) DrawAnimatedCovers(RIGHT); break; } //Принудительно вызывает перерисовку экрана glutPostRedisplay(); } void keyboard(unsigned char key, int x, int y) { switch(key) { //Выход по Esc case 27: ClearCovers(); exit(0); break; } //Принудительно вызывает перерисовку экрана glutPostRedisplay(); } void mouse(int x, int y) { //Обрабатывает движение мышь при нажатии любой кнопки static int prevX = 500; if(x - prevX > 0 && !rightLocked) DrawAnimatedCovers(RIGHT); else if(x - prevX < 0 && !leftLocked) DrawAnimatedCovers(LEFT); prevX = x; //Принудительно вызывает перерисовку экрана glutPostRedisplay(); } int main (int argc, char * argv[]) { glutInit(&argc, argv); //Включаем двойную буферизацию, четырехкомпонентный цвет, z-буфер и мультисемплинг (сглаживание) glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_MULTISAMPLE); //Задаем размеры окна glutInitWindowSize(W_WIDTH, W_HEIGHT); //Задаем стартовое положение окна glutInitWindowPosition(100, 100); //Название окна glutCreateWindow("Cower Flow"); #ifdef _WIN32 //Скрываем окно консоли в Windows FreeConsole(); #endif //Вызываем свою функцию инициализации init(); //Задаем callback'и glutReshapeFunc(reshape); glutDisplayFunc(display); glutMotionFunc(mouse); glutKeyboardFunc(keyboard); glutSpecialFunc(keyboardSpec); glutMainLoop(); return 0; }
Терминология: Двойная буферизация Альфа-смешивание Мультисемплинг (антиалиасинг) Z-буферизация
Итог
Для тех, кто не смог (или не захотел) осилить статью: код, бинарные файлы для Windows, Linux и Mac OS X (все 32-битные), обложки для теста.
выход по Esc :D
ОтветитьУдалитьА если серьезно, то ты очень круто сделал)
Интересная статься, спасибо.
ОтветитьУдалить