?
Графические эффекты (Эффект линзы, вращающегося цилиндра, конуса и сферы с рисунками на них)Аннотация: Когда в программу "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г. На главную страницу сайта автора
|