?
Графические эффекты (Эффект линзы, вращающегося цилиндра, конуса и сферы с рисунками на них)Аннотация: Когда в программу "Relaxation" - программа для расслабления зрения мне захотелось добавить несколько модных эффектов (скоро выйдет новая версия этой популярной программы, в которую они будут включены), то, как всегда, я прогуглил интернет. Но ничего приемлемого для эффектов линзы, вращающихся цилиндра, конуса и сферы с рисунками на них я так и не нашёл. Единственный пример, найденный с описанием и кодом, алгоритм эффекта линзы, не мог быть переписанным на C# из-за ассемблерных вставок, все другие примеры грешили сложной математикой, но нужного эффекта после их реализации в коде не давали. Что касается цилиндра, конуса и сферы, то кроме WPF реализации ничего в интернете нет. Пришлось решать поставленные задачи самому. И, как всегда, когда что то начинаешь делать, то оказывается оно не так уж и сложно. А классические примеры из презентации WPF (вращающиеся цилиндр, конус и сфера с рисунками на них), вполне реализуемы и без WPF, с использованием GDI+. Этому и посвящена данная статья, как просто можно реализовать старыми добрыми способами, достаточно сложные графические эффекты.
1. Эффект ЛинзыАлгоритм линзы, в указанной в аннотации статье, трактуется так: Как видно, предлагается рисовать изображение под линзой по точкам. Но и автор этого алгоритма понимает, что рисовать каждую точку из всей совокупности - это долго. И поэтому не зря он предлагает "заранее просчитать таблицу смещений". Этот алгоритм не сложен. В принципе его можно дополнить, взяв все биты изображения в массив указателей на пиксели, используя небезопасный контекст(unsafe) и класс Marshal. Далее, все указанные математические преобразования, можно выполнить уже в данном массиве, после чего вернуть изменённые пиксели изображения на то место, которое они должны принимать. Однако есть способ и проще - взять кусок рисунка, который надо увеличить, растяyть его методами GDI (функция DrawImage), обрезать по форме линзы и скопировать в место, над которым находится линза. Именно применение этого метода показано на Рис.1. Проект решения имеет на главной форме кон
Рис.1. Эффект линзы тролы PictureBox, Timer, OpenFileDialog, SaveFileDialog и MenuStrip. MenuStrip имеет пункты добавления и удаления рисунка и выбора рисунков. При старте первый рисунок, который вы видите выше, формируется из ресурсов программы и сохраняется в директории программы Image под именем 01start.jpg. Все эффекты подцепляются как классы эффектов. Для взаимодействия с классом в центральной форме:
Код класса всех эффектов не сложен, в его понятии помогут встроенные коментарии. using System; using System.Collections.Generic; using System.Text; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Threading; using System.IO; namespace Lens { class clLens { //Ссылка на класс основной форме передаётся в класс эффекта для //возможности использования параметров и переменных основной формы private Form1 myForm; //Все байты исходного рисунка последовательно через ссылки private byte[] bmpBytesSource; //Все байты исходного рисунка разбитые по массивам составляющих ARGB private byte[,] brgPixelsB; private byte[,] brgPixelsG; private byte[,] brgPixelsR; private byte[,] brgPixelsL; //Ссылки на байты сформированного изображения в линзе private byte[] bmpBytes; //Временные параметры для самостоятельного движения линзы private DateTime dt1 = DateTime.Now; private DateTime dt2 = DateTime.Now; private TimeSpan ts = DateTime.Now - DateTime.Now; //Направление самостоятельного движения линзы private bool fKudaX = false; private bool fKudaY = false; //Для определения направлений движения public Random r = new Random(DateTime.Now.Millisecond); //Переменные хранения рисунков private static Bitmap bmpStart = null; private Bitmap bitmapWork { get; set; } private Bitmap bmpTemp { get; set; } private Bitmap bmpTemp1 { get; set; } //Вспомогательная переменная private bool fBlock = false; //Диаметр линзы private int viDiamLens = 0; //Размеры рисунка private int viWidth = 0; private int viHeight = 0; //Рабочий класс Graphics public Graphics graph = null; //Переменные координат точки линзы private int viPointX = 0; private int viPointY = 0; private int viPointHideX = 0; private int viPointHideY = 0; private int viPointWorkX = 0; private int viPointWorkY = 0; private int viPointWorkHideX = 0; private int viPointWorkHideY = 0; private ImageAttributes imageattributes = new ImageAttributes(); //Вспомогательные переменные private int viI { get; set; } private int viJ { get; set; } //Конструктор public clLens(Form1 form1) { myForm = form1; } //Установки при старте и смене размера окна программы и рисунка public void vNewSize() { if (fBlock) return; fBlock = true; //Получаем адрес рисунка, в классе основной форме //все имена рисунков находятся в массиве myForm.srgFiles string s = myForm.srgFiles[myForm.viImage]; if (!File.Exists(s)) { fBlock = false; return; } //Картинка из памяти bmpTemp = new Bitmap(s); //Делаем размер формы под размер картинки myForm.StartPosition = System.Windows.Forms.FormStartPosition.Manual; myForm.Width = bmpTemp.Width + 10; myForm.Height = bmpTemp.Height + 62; myForm.Left = (myForm.screen.Width - myForm.Width) / 2; myForm.Top=(myForm.screen.Height- myForm.Height) / 2; //Размер pictureBox1 под размер картинки myForm.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.CenterImage; myForm.pictureBox1.Dock = System.Windows.Forms.DockStyle.None; myForm.pictureBox1.Width = bmpTemp.Width; myForm.pictureBox1.Height = bmpTemp.Height; myForm.pictureBox1.Left = (myForm.Width - bmpTemp.Width) / 2; myForm.pictureBox1.Top = (myForm.Height - bmpTemp.Height) / 2; viWidth = myForm.pictureBox1.Width; viHeight = myForm.pictureBox1.Height; //Освобождаем ссылку на выбранный файл рисунка bmpTemp.Dispose(); //Исходный рисунок перенесём в bmpStart bmpStart = new Bitmap(viWidth, viHeight); graph = Graphics.FromImage(bmpStart); graph.DrawImage(bmpTemp, new Rectangle(0, 0, viWidth, viHeight), 0, 0, bmpTemp.Width, bmpTemp.Height, GraphicsUnit.Pixel); bitmapWork = new Bitmap(viWidth, viHeight); bmpTemp.Dispose(); //Забираем ссылки на байты изображения в массив bmpBytesSource = new Byte[viWidth * viHeight * 4]; BitmapData bmpTempDataStart = bmpStart.LockBits(new Rectangle(0, 0, viWidth, viHeight), ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb); unsafe { try { Marshal.Copy(bmpTempDataStart.Scan0, bmpBytesSource, 0, viWidth * viHeight * 4); } catch (Exception) { } finally { bmpStart.UnlockBits(bmpTempDataStart); bmpTempDataStart = null; } } //Задаём размер линзы viDiamLens = (viWidth >= viHeight) ? viHeight : viWidth; viDiamLens = viDiamLens / 4; while (viDiamLens % 2 != 0 && viDiamLens % 4 != 0) { viDiamLens++; } //Устанавливаем вспомогательные переменные viPointX = 0; viPointY = 0; fKudaX = false; fKudaY = false; ts = DateTime.Now - DateTime.Now; myForm.StartPosition = System.Windows.Forms.FormStartPosition.Manual; fBlock = false; return; } //Действия выполнеемые по времени public void LineTick() { if (fBlock) return; fBlock = true; //Рисунок для изображения в линзе (bmpTemp используем повторно, он свободен) bmpTemp = new Bitmap(viDiamLens, viDiamLens); graph = Graphics.FromImage(bmpTemp); //Заливаем красным graph.Clear(Color.FromArgb(255, 0, 0)); //Рисцем элипс(круг, в размер линзы), залитый зелёным цветом на красном фоне graph.FillEllipse(new SolidBrush(Color.FromArgb(0, 255, 0)), -1, -1, viDiamLens, viDiamLens); graph.Dispose(); //Берём ссылки на пиксели в bmpBytesRedGreen byte[] bmpBytesRedGreen = new Byte[bmpTemp.Width * bmpTemp.Height * 4]; BitmapData bmpTempData = bmpTemp.LockBits(new Rectangle(0, 0, viDiamLens, viDiamLens), ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb); unsafe { int* pbmpTemptData = null; try { pbmpTemptData = (int*)bmpTempData.Scan0;//указатель на первый байт bmpStartData Marshal.Copy(bmpTempData.Scan0, bmpBytesRedGreen, 0, viDiamLens * viDiamLens * 4); } catch (Exception) { } finally { bmpTemp.UnlockBits(bmpTempData); bmpTempData = null; pbmpTemptData = null; bmpTemp.Dispose(); } } //Этот кусок кода необходим для определения места расположения линзы //Определяем стоит ли линзе самостоятельно менять положение //если указатель мыши не двигался установленное время viPointX = myForm.viMoseX; viPointY = myForm.viMoseY; if (viPointX == viPointHideX && viPointY == viPointHideY) { dt2 = DateTime.Now; ts = dt2 - dt1; if (ts.TotalSeconds > 5) { if (fKudaX) { viPointWorkX = viPointWorkHideX + r.Next(1, 5); } else { viPointWorkX = viPointWorkHideX - r.Next(1, 5); } if (fKudaY) { viPointWorkY = viPointWorkHideY + r.Next(1, 5); } else { viPointWorkY = viPointWorkHideY - r.Next(1, 5); } viPointWorkHideX = viPointWorkX; viPointWorkHideY = viPointWorkY; } } else { dt1 = DateTime.Now; viPointHideX = viPointX; viPointHideY = viPointY; viPointWorkHideX = viPointX; viPointWorkHideY = viPointY; viPointWorkX = viPointX; viPointWorkY = viPointY; } if (viPointWorkX == 0 && viPointWorkY == 0) { viPointWorkX = viWidth / 2; viPointWorkY = viHeight / 2; } if (viPointWorkX - viDiamLens / 2 <= 0) { fKudaX = true; viPointWorkX = viDiamLens / 2; } if (viPointWorkX + viDiamLens / 2 >= viWidth) { fKudaX = false; viPointWorkX = viWidth - viDiamLens / 2; } if (viPointWorkY - viDiamLens / 2 <= 0) { fKudaY = true; viPointWorkY = viDiamLens / 2; } if (viPointWorkY + viDiamLens / 2 >= viHeight) { fKudaY = false; viPointWorkY = viHeight - viDiamLens / 2; } //Делаем увеличенную картинку под линзой bmpTemp1 = new Bitmap(2 * viDiamLens, 2 * viDiamLens); graph = Graphics.FromImage(bmpTemp1); graph.DrawImage(bmpStart, new Rectangle(0, 0, 2 * viDiamLens, 2 * viDiamLens), viPointWorkX - viDiamLens/2, viPointWorkY - viDiamLens/2, viDiamLens, viDiamLens, GraphicsUnit.Pixel); graph.Dispose(); //Копируем часть увеличенной картинки в картинку линзы bmpTemp = new Bitmap(viDiamLens, viDiamLens); graph = Graphics.FromImage(bmpTemp); graph.DrawImage(bmpTemp1, new Rectangle(0, 0, viDiamLens, viDiamLens), bmpTemp1.Width/2 - viDiamLens / 2, bmpTemp1.Height/2 - viDiamLens / 2, viDiamLens, viDiamLens, GraphicsUnit.Pixel); graph.Dispose(); //Берём с неё все пиксели увеличенной картинки byte[] bmpBytesBig = new Byte[bmpTemp.Width * bmpTemp.Height * 4]; bmpTempData = bmpTemp.LockBits(new Rectangle(0, 0, viDiamLens, viDiamLens), ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb); unsafe { try { Marshal.Copy(bmpTempData.Scan0, bmpBytesBig, 0, viDiamLens * viDiamLens * 4); } catch (Exception) { } finally { bmpTemp.UnlockBits(bmpTempData); bmpTempData = null; bmpTemp.Dispose(); } } brgPixelsB = new byte[viWidth, viHeight]; brgPixelsG = new byte[viWidth, viHeight]; brgPixelsR = new byte[viWidth, viHeight]; brgPixelsL = new byte[viWidth, viHeight]; //Забираем в массивы все биты источника, рассортировав их по цветам for (int i = 0; i < viWidth; i++) { for (int j = 0; j < viHeight; j++) { brgPixelsB[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4)]; brgPixelsG[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4) + 1]; brgPixelsR[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4) + 2]; brgPixelsL[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4) + 3]; } } //Туда куда надо кладем из увеличенной, там где зелёный круг for (int i = 0; i < viDiamLens; i++) { for (int j = 0; j < viDiamLens; j++) { if (bmpBytesRedGreen[i * 4 + (j * viDiamLens * 4) + 1] == 255) { brgPixelsB[viPointWorkX - viDiamLens/ 2 + i, j + viPointWorkY - viDiamLens / 2] = bmpBytesBig[i * 4 + (j * viDiamLens * 4)]; brgPixelsG[viPointWorkX - viDiamLens / 2 + i, j + viPointWorkY - viDiamLens / 2] = bmpBytesBig[i * 4 + (j * viDiamLens * 4) + 1]; brgPixelsR[viPointWorkX - viDiamLens / 2 + i, j + viPointWorkY - viDiamLens / 2] = bmpBytesBig[i * 4 + (j * viDiamLens * 4) + 2]; brgPixelsL[viPointWorkX - viDiamLens / 2 + i, j + viPointWorkY - viDiamLens / 2] = bmpBytesBig[i * 4 + (j * viDiamLens * 4) + 3]; } } } //Возвращаем байты увеличенной чпсти на место в bitmapWork bmpBytes = new byte[viWidth * 4 * viHeight]; for (int i = 0; i < viWidth; i++) { for (int j = 0; j < viHeight; j++) { bmpBytes[i * 4 + (j * viWidth) * 4] = brgPixelsB[i, j]; bmpBytes[i * 4 + (j * viWidth * 4) + 1] = brgPixelsG[i, j]; bmpBytes[i * 4 + (j * viWidth * 4) + 2] = brgPixelsR[i, j]; bmpBytes[i * 4 + (j * viWidth * 4) + 3] = brgPixelsL[i, j]; } } BitmapData bitmapData = bitmapWork.LockBits(new Rectangle(0, 0, viWidth, viHeight), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb); unsafe { Marshal.Copy(bmpBytes, 0, bitmapData.Scan0, bmpBytes.Length); } bitmapWork.UnlockBits(bitmapData); graph = Graphics.FromImage(bitmapWork); //Обрамляем линзу graph.DrawEllipse(new Pen(Color.Gray, 2), viPointWorkX - viDiamLens / 2, viPointWorkY - viDiamLens / 2, viDiamLens, viDiamLens); graph.Dispose(); //Перендаём в pictureBox1 myForm.pictureBox1.Image = bitmapWork; fBlock = false; return; } } } 2. Вращающийся цилиндр с рисунками на нёмРеализация алгоритма вращающегося цилиндра в приведённом примере выполнена по аналогии с предыдущим примером. в отдельном классе "class clCylinder". Напомним, что проект решения имеет на главной форме контролы PictureBox, Timer, OpenFileDialog, SaveFileDialog и MenuStrip. MenuStrip имеет пункты добавления и удаления рисунка и выбора рисунков. При старте первый рисунок, который вы видите выше, формируется из ресурсов программы и сохраняется в директории программы Image под именем 01start.jpg. В коде центральной формы содаётся класс "clcylinder", который объявлен как: private clCylinder clcylinder = null; Сразу после созданя вызывается уже известная нам функция класса "vNewSize()", задача которой выполнить подготовительную работу. Основная задача этой работы забрать все байты рисунка в массив, распределить их в массивы по цветам. Кроме того определить насколько потребуется сдвинуть по форме элипса пиксели исходного рисунка для переднего плана вниз, а для заднего плана вверх. clcylinder = new clCylinder(this); clcylinder.vNewSize(); clcylinder = new clCylinder(this); Остальные пояснения представлены непосредственно в тексте приведённого кода. Результат показан на Рис.2.
Рис.2. Вращающийся цилиндр //Исходный код класса using System; using System.Collections.Generic; using System.Text; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; namespace Cylinder { class clCylinder { private byte[,] brgPixelsB; private byte[,] brgPixelsG; private byte[,] brgPixelsR; private byte[,] brgPixelsL; private int viI; private int viJ; private double radB; private double radA; private int radB1; private Form1 myForm; private bool fBlock = false; private Bitmap bmpTemp { get; set; } private int viWidth = 0; private int viHeight = 0; public Graphics graphFore = null; public Graphics graphBack = null; public Graphics graph = null; private byte[] bmpBytesSource; private byte[] bmpBytesFore; private byte[] bmpBytesBack; //Исходная в половину private static Bitmap bmpStart = null; private Bitmap bitmapWorkFore { get; set; } private Bitmap bitmapWorkBack { get; set; } private Bitmap bitmapWork { get; set; } private Point[] ptEllipceFore; private Point[] ptEllipceBack; public clCylinder(Form1 form1) { myForm = form1; } internal void vNewSize() { if (fBlock) return; fBlock = true; //Берём дорогу картинки string s = myForm.srgFiles[myForm.viImage]; //Картинка из памяти помещаем в bmpTemp bmpTemp = new Bitmap(s); viWidth = bmpTemp.Width; viHeight = bmpTemp.Height; while (viWidth % 2 != 0) { viWidth--; } if (viWidth < 20 || viHeight < 20) { bmpTemp.Dispose(); return; } bmpStart = new Bitmap(viWidth, viHeight); graphFore = Graphics.FromImage(bmpStart); graphFore.DrawImage(bmpTemp, new Rectangle(0, 0, viWidth, viHeight), 0, 0, bmpTemp.Width, bmpTemp.Height, GraphicsUnit.Pixel); //Освобождаем картинку в памяти bmpTemp.Dispose(); graphFore.Dispose(); //Готовим массив ссылок на байты ARJB картинки bmpBytesSource = new Byte[viWidth * viHeight * 4]; BitmapData bmpTempDataStart = bmpStart.LockBits(new Rectangle(0, 0, viWidth, viHeight), ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb); unsafe { try { //Забираем все ссылки на байты картинки Marshal.Copy(bmpTempDataStart.Scan0, bmpBytesSource, 0, viWidth * viHeight * 4); } catch (Exception) { } finally { bmpStart.UnlockBits(bmpTempDataStart); bmpTempDataStart = null; } } //Распределяем ссылки в массивы пикселей по цветам brgPixelsB = new byte[viWidth, viHeight]; brgPixelsG = new byte[viWidth, viHeight]; brgPixelsR = new byte[viWidth, viHeight]; brgPixelsL = new byte[viWidth, viHeight]; //Забираем в массивы все биты источника в массивы по цветам for (int i = 0; i < viWidth; i++) { for (int j = 0; j < viHeight; j++) { brgPixelsB[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4)]; brgPixelsG[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4) + 1]; brgPixelsR[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4) + 2]; brgPixelsL[i, j] = bmpBytesSource[i * 4 + (j * viWidth * 4) + 3]; } } //Задаём радиусы элипсов для отрисовки верхней и нижней части вращающегося цилиндра //Крышка и донышко radB = bmpStart.Width / 2 / 20; radA = Math.Sqrt(((double)bmpStart.Width * (double)bmpStart.Width / (2 * (Math.PI * Math.PI)) - radB * radB)); radB1 = (int)radB; //Прямоугольник залит синим цветом, внутри нарисован элипс краснам цветом //По красному будем определять границы в которых нужно брать пиксели Bitmap bmp = new Bitmap(2 * (int)radA, 2 * radB1); graphFore = Graphics.FromImage(bmp); graphFore.Clear(Color.Blue); graphFore.DrawEllipse(new Pen(Color.FromArgb(255, 0, 0), 1), 0, 0, 2 * (int)radA - 1, 2 * radB1 - 1); Color cl; List 3. Вращающаяся сфера (шар) с рисунками на нёйВ создание эффекта заложены аналогичные принципы, что и при создании эффекта цилиндра (Рис.3.).
Рис.3. Вращающаяся сфера 4. Вращающийся конус с рисунками на нёмВ создание эффекта заложены аналогичные принципы, что и при создании эффекта цилиндра (Рис.4.).
Рис.4. Вращающийся конус 5. Скачать исходные кодыВращающийся цилиндр с рисунком на нём ~ 2810 кб. Вращающийся кунус с рисунком на нём ~ 2810 кб. Вращающаяся сфера с рисунком на ней ~ 3560 кб. Молчанов Владислав 30.09.2015г. На главную страницу сайта автора
|