суббота, 19 декабря 2009 г.

SilverNotes: Drag'n'Drop + Print + RightClick

Привет, Друзья!

Сегодняшний пост посвящен сразу трем новинкам Silvelright 4.0: Printing, Drag'n'Drop и Right Click.

Надо отдать должное разработчикам Silverlight: они сделали настолько простой API для рассматриваемых новинок, что их описание влезло бы в два-три твита (интересно, твит - это уже стандартизированная единица размера информации :)? ).

В сумме эта тройка позволила мне за пару часов написать маленькое, но полезное приложение: SliverNotes (исходники, демо - для демо нужен 4-й сильверлайт). Эта программка позволяет сделать из обычного листка формата A4 карманную книжечку на 8 министраниц. Формат и содержание каждой странички определяется пользователем. Например, это может быть список покупок, подготовленный любимой женой забывчевому мужу, или же каждая страница может быть нотоносцем для талантливого музыканта, или... все что захотите. Вот пример распечатанной карманной книжки:



Итак к делу! (Иначе я рискую сделать введение самой длинной и сложной частью этого поста :)...)

Печать


Все, что нужно от разработчика для вывода содержимого на печать, это вызов функции Print() у объекта типа PrintDocument:
// PrintDocument - тот, кто нам нужен для печати.
var doc = new PrintDocument { DocumentName = "Notes" };

doc.PrintPage += (o, e) =>
                   {
                     e.PageVisual = uie;
                     e.HasMorePages = false;
                   };

doc.Print();
В обработчике события PrintPage  мы говорим Silverlight'у что именно нужно напечатать (e.PageVisual = uie). Здесь uie - это объект типа UIElement. Если бы мы хотели распечатать больше, чем одну страницу, то свойство e.HasMorePages нужно установить в true.

Чуть-чуть деталей

  • Если элемент, который вы выводите на печать "обрезается" родительским контейнером, он будет обрезанным и на бумаге. Чтобы это обойти, просто измените родительский контейнер, например, на Canvas.
  • Если вы применяете трансформации прямо к элементу, который служит PageVisual'ом, то они не будут применены на печати. Чтобы это обойти, устанавливайте трансформации ниже по дереву: вместо самого PageVisual'a, трансформируйте его первого ребенка (жуть-то какая :) ).
  • PageVisual не обязательно должен находиться в Visual Tree. Это может быть просто объект в памяти.
  • Инициировать печать можно только по запросу пользователя (например, в обработчике Click'a). Эй, а где еще его можно инициировать-то, умник? - Ну, у меня было желание стартовать печать в событии Loaded, но Silverlight пожелал мне удачи в виде приветливого Exception'a. Даже если обработчик Loaded - это лямбда в обработчике Click, вы получите исключение...

Drag'n'Drop


Эта новинка позволяет пользователям перетаскивать в плагин [пока только] файлы из операционной системы. Стоит ли говорить, на какой уровень это повышает UX (user experience)? . Фантазия - ваш предел. В SilverNotes вы можете перетащить на пустую страничку картинку, тем самым сделав свой шаблон для печати:


1. Слева - окошко Windows Explorer'a. Я схватил фотографию леса из моего родного города Лебедина, и тащу ее на SilverNotes. Превью фото делает Explorer.

2. Теперь указатель мышки находится над пустой страничкой SilverNote's. Я замечаю это в событии DragEnter, и начинаю анимировать цвет фона, как бы говоря: "Давай, бросай!"
 
3. Ну и, наконец, я отпускаю левую кнопку, и фото оказывается в Silverlight приложении...

Для того, чтобы разрешить бросать файл на UIElement, достаточно установить свойство AllowDrop="True". После этого у UIElement'a будут срабатывать обработчики событий DragEnter, DragLeave, DragOver и Drop. Вот пример кода, который считывает брошенную картинку на UIElement с установленным AllowDrop'ом:

private void DoDrop(object sender, DragEventArgs e)
{
  if (e.Data == null)
  {
    return; // И почему мы так не любим nulls?
  }
  
  // Куда именно это мы бросаем?
  Point dropPosition = e.GetPosition(_cnvDropTarget);

  // Ну-ка, что нам тут подбросили?
  var files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];

  foreach (var file in files)
  {
    using (var stream = file.OpenRead())
    {
      // Обрабатываем файл:
      ProcessFileContent(stream, dropPosition);
    }
  }
}

Чуть-чуть деталей

  • Не пытайтесь получить доступ к данным во время событий DragEnter/DragLeave. Только разочарование и SecurityException ждут вас. Правильное место для считывания данных - обработчик события Drop.
  • Не пытайтесь перетаскивать файл на Silverlight приложение, с установленным windowless режимом. Также проблематичным будет использование Drag'n'Drop под Маками (см. детали).

ПКМ ака RightClick


О поддержке правого клика в сильверлайт приложениях раньше приходилось только мечтать (или извращаться с html'ем поверх сильверлайта). Теперь же, все что нужно сделать - это подписаться на событие MouseRightButtonDown (или MouseRightButtonUp). Если вы хотите показать свое контекстное меню по правому клику, то нужно... написать свое контекстное меню и показать его. Что?! В сильверлайте нет стандартной реализации класса ContextMenu? - Да, покамест нет. Но не удивлюсь, если с выходом четверки (не беты) оно там появится. Пока же Microsoft предлагает пользоваться классом Popup, и в него встраивать контекстное содержимое.

Для демонстрации на IT Jam'e мы воспользовались кодом Jesse Bishop'a, и чуть-чуть изменили его, сделав более похожим на стандартное WPF меню:



В SilverNotes контекстное меню для Grid'a устанавливает следующий код:

<Grid x:Name="LayoutRoot" Background="Transparent">
  <menu:ContextMenu.ContextMenu>
 <menu:ContextMenu>
   <menu:MenuItem Click="OnDeleteClick">
  <menu:MenuItem.Content>
    <StackPanel Orientation="Horizontal">
   <Image Margin="0, 0, 8, 0" Source="/SilverNotes;Component/Resources/delete.png" Width="16" Height="16"/>
   <TextBlock Text="Delete"/>
    </StackPanel>
  </menu:MenuItem.Content>
   </menu:MenuItem>
 </menu:ContextMenu>
  </menu:ContextMenu.ContextMenu>
  <!-- Содержимое Grid. -->
</Grid>

Вы также можете пользоваться и улучшать код проекта, скачав исходники SilverNotes.

Чуть-чуть деталей

  • Чтобы полностью скрыть стандартное меню сильверлайта, не забудьте установить e.Handled = true, в обработчике MouseRightButtonDown.
  • Событие MouseRightButtonUp не прийдет к вам, до тех пор пока вы не установите e.Handled = true в MouseRightButtonDown.

Итак


Все три новинки наглядно показаны в SilverNotes. Я выложил этот проект на Google Code. Если у вас есть желание усовершенствовать проект, дописать шаблоны для печати - добро пожаловать :). Кстати, создавать свои собственные шаблоны в SilverNotes так же просто, как создавать обычный UserControl на Silverlight'e. Шаблоны загружаются при помощи MEF (Managed Extensibility Framework), который так же входит в поставку четвертого Silverlight. Но об этом чуть-чуть попозже :).

Отличного настроения и прекрасного программирования в предновогодние морозы :)!

понедельник, 7 декабря 2009 г.

Webcam Fun

Привет, Друзья!

На всеукраинской сходке айтишников IT Jam, нам с Сергеем Лутаем посчастливилось делать доклад по новинкам четвертого Сильверлайта. Пока обрабатывается видео с презентации, выкладываю код, который мы использовали для демонстрации работы с веб камерой. Кроме кода, мы посмотрим на основы работы с камерой  и на несколько веселых эффектов, которые можно сделать при помощи пиксельных шейдеров.

Исходники к этому посту можно скачать здесь. Живое демо находится здесь. Для запуска демо, вам, кроме камеры, понадобится установить Silverlight 4 Beta 1. Рантайм есть только для разработчиков, но ведь это про вас, верно :)?

Основы


Для старта нам понадобится... камера. Чтобы получить доступ к камере из Silverlight'a, достаточно спросить у пользователя разрешения, и получив оное, начать видеозахват:

private CaptureSource _captureSource;

private CaptureSource CaptureSource
{
  get { return _captureSource ?? (_captureSource = new CaptureSource()); }
}

private void StartCapture()
{
  // Можно мне попользоваться вашей камерой?
  if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess())
  {
    // Урра! Начинаем захват: 
    CaptureSource.Start();
    
    var brush = new VideoBrush();
    brush.SetSource(CaptureSource);
    VideoBrush = brush; // К этому свойству можно привязать любой Brush: <Border Background="{Binding VideoBrush}" .../>
  }
}
Видео является обычной кисточкой (Brush), а значит, мы можем разукрасить выходом с камеры любой элемент, который можно разукрасить :). Пожалуй, это и все. Просто, не так ли?

Не Основы


К "продвинутым" темам можно отнести сохранение рисунка с камеры. Тут есть два варианта: воспользоваться функцией CaptureSource.AsyncCaptureImage() или сделать снимок UIElement'a, как обычно, при помощи WriteableBitmap'a: new WriteableBitmap(uie, null). Разница в том, что WriteableBitmap фотографирует содержимое со всеми эффектами, а CaptureSource выдаст именно то, что есть на выходе камеры, без трансформаций, эффектов и прочего.

Если есть несколько камер (скорее всего, у вас, как и у меня, их нет, но мало ли :)?), то можно всегда выбрать с какой камеры вести захват: CaptureDeviceConfiguration.GetAvailableVideoCaptureDevices();

Для того, чтобы сохранить WriteableBitmap в Jpeg формат нужно написать свой энкодер, который будет последоватльно, байт за байтом, запаковывать цвета в формат Jpeg. Для этого, конечно, нужно сначала достать спецификацию Jpeg'a, изучить ее, и заимплементить... Шутка :). Все уже написано до нас: библиотека FJCore позволяет легко сохранять jpeg'и. Для просмотра примера использования, загляните в исходники к этому посту.

Спецэффекты


Использование спецэффектов радует больше всего. В реальном времени увидеть себя с необычной стороны, и я говорю не о спине, - оказывается очень увлектально :)! О создании собственных эффектов на шейдерах, я уже писал. А на презентации мы показали четыре эффекта:

Эффект находит границы на рисунке и закрашивает их черным цветом. Все остальное выводится белым. На этой фотке знакомые могут узнать Мишу Чалого :).
С этой фотки из паралельной вселенной дружественный привет землянам шлет головастый и чертовски умный двойник  автора этого блога :).
Этот эффект-страшилка просто инвертирует цвета. Как-то так выглядел мир по ту сторону кольца, когда Фродо надевал его :).
И, наконец, последний эффект, "скрытая фича Sivlerlight 4.0", он же знакомый нам по передачам типа "Прогноз погоды": подмена вывода. Как только цвет точки становится ярче определенного порога, вместа оригинала выводится точка из другого сэмплера (читай - brush'a). Здесь я направил включенный фонарик в ладошку, открывая секретный кусочек лица Билла.

Конечно, можно написать шейдер, который будет подменять вывод в зависимости от разных условий, например, на синий цвет выводить Брина и Пейджа, на зеленый - Стива, а на очень белый (или черный, в зависимости от того, на чьей вы стороне) - Билла. 

Выводы


Пользоваться камерой в Silverlight  4.0 очень легко. Все операции с камерой не требуют  участия сервер-сайда. Можно легко сохранять изображения.

Для тех, кто любит просто читать с конца, еще раз привожу ссылку на исходники и демо. Не забудьте про рантайм: Silverlight 4 Beta 1. Отличного программирования и прекрасного настроения!

ЗЫЖ спасибо всем, кто пришел к нам на презентацию. И, конечно, всем тем, кто поставил за нас "наляпочки" на доске голосования :). Нам было очень приятно!

среда, 8 июля 2009 г.

July 10: See the Light

Привет :)!

Немножко пафосное название, но из домена букв не выбросишь. http://www.seethelight.com/ анонсирует выпуск третьего сильверлайта и бленда уже 10-го июля. Согласно MSDN Flash'у, виртуальный запуск состоится именно там.

Ждем-ждем тяпницы!

среда, 10 июня 2009 г.

Silverlight vs WPF

Привет!


Silverlight, обычно, называют подмножеством WPF и не зря. Стили программирвания на WPF и на Silverlight очень похожи друг на друга: (XAML + Code Behind) * .NET. Но за этим сходством кроется опасность. Например, программируя на WPF'e, можно ожидать, что Silverlight так же поддерживает создание своих собственных расширений разметки (markup extension). А он, козявка, и не думает. Таких тонких различий, компания Wintellect насобирала на 70 страниц в документе Guidance on Differences Between WPF and Silverlight. Помимо различий/сходств парни дают советы, как обойти некоторые ограничения "подмножества".

Надеюсь, в скором времени число страниц в документе уменьшится, как и различий между Silverlight и WPF :).

Отличного программирования, друзья!

PS: XAML читается как "замл" или "зэмл", а не "икс-эй-эм-эл" и не "кзамл". Кстати, изначально аббревиатура означала Extensible Avalon Markup Language, а не Extensible Application Markup Language.

воскресенье, 17 мая 2009 г.

TechTalks: Sackoverflow

Привет!

К своему огромному стыду только что узнал, что stackoverflow.com был создан Джоэлом Спольки (киньте в меня чем-то, за темноту).

О том, почему сайт пользуется такой популярностью, Джоэл рассказывает на TeckTalk'e в Google. Очень рекомендую:

среда, 29 апреля 2009 г.

Magic Shaders: Genie Effect

Привет!

Мой коллега, счастливый обладатель MacBook'a, показал мне сногсшибательный эффект сворачивающегося окошка в Mac OS'e: Genie effect (эффект джина). Вот как он выглядит:



Тогда я загорелся идеей сделать аналогичный эффект в Silverlight приложении, но не знал как :). Последнюю неделю я провел за изучением пиксельных шейдеров и к огромной моей радости обнаружил ключ к разгадке. В скором времени родилась библиотека, реализующая Genie-эффект, как в WPF'e, так и в Silverlight'e.  Посмотреть живую демку эффекта можно здесь (нужен 3-й сильверлайт: link). В этом посте мы:
  • Посмотрим как использовать Genie-эффект у себя в коде.
  • Посмотрим как создаются собственные эффекты на базе пиксельных шейдеров.
  • Разберем алгоритм Genie-эффекта
Ссылка на исходники к статье находится в конце поста. Исходники распространяются под LGPL лицензией.

NB: Apple Inc. является обладателем патента на этот эффект в США. Надеюсь, я не нарушаю их прав, описывая в исключительно учебных целях один из возможных вариантов создания подобного эффекта на WPF/Silverlight технологиях.

GenieLib в действии


GenieLib - это моя библиотека, реализующая описанный эффект Джина (исходники - в конце поста). Для того, чтобы создать Genie-эффект в вашем приложении достаточно добавить ссылку на сборку GenieLib и определить два UIElement'a: один будет представлять Джина, а второй - Лампу (место, куда прячется Джин).

Вот, как это выглядит в коде:

XAML:
<Button Click="ToggleGenie"
 x:Name="btnLamp"
        Content="Lamp"/>
<Image x:Name="imgGenie" Source="...">
<Image.RenderTransform>
    <TranslateTransform />
</Image.RenderTransform>
</Image>

C#
private void ToggleGenie()
{
  GenieManager.IsGenieOut = !GenieManager.IsGenieOut;
}

private Magic GenieManager
{
  get
  {
    if (_genieManager == null)
    {
      _genieManager = new Magic(btnLamp, imgGenie);
      _genieManager.Expanding +=
        (o, e) =>
        {
          imgGenie.Visibility = Visibility.Visible;
        };

      _genieManager.Collapsed +=
        (o, e) =>
        {
          imgGenie.Visibility = Visibility.Collapsed;
        };
    }
    return _genieManager;
  }
}

Как видите, мы определили два UIElement'a в xaml-файле, и по нажатию на кнопку, которая по совместительству подрабатывает волшебной лампой, мы обращаемся к объекту типа Magic, инвертируя текущее состояние Джина. Не смотря на название, класс Магии не делает ничего таинственного. В его обязанности входит построение анимации перехода Джина в Лампу и из нее. Класс не накладывает никаких ограничений на представителей Лампы и Джина. Но для правильной работы он ожидает от Джина наличия одного RenderTransform'a: TranslateTransform. Именно этот трансформ позволяет Джину "летать" в пространстве по направлению к лампе и от нее. Если это требование не соблюдается, Джин будет уменьшаться в рамках собственной области, не приближаясь к лампе (картинка слева):



Размер и позиция Лампы относительно Джина играют важную роль. Позиция определяет в какую сторону будет лететь Джин при сворачивании, а размер Лампы (ширина или высота, в зависимости от направления сворачивания) указывает Джину насколько сильно нужно сузиться, чтобы "влезть" в горлышко Лампы :).

Помимо этого класс Magic уведомляет клиентов  при помощи событий о специфических состояниях Джина (собирается спрятаться в лампу, спрятался, собирается выйти из лампы и вышел из лампы). Клиенты могут реагировать на события должным образом. В нашем случае, мы убираем Джина, устанавливая его свойство Visibility в Visibility.Collapsed, когда Джин спрятался в Лампу, и показываем его, когда Джин собирается наружу. Забегая вперед, скажу, что эффект, который реализует уменьшение Джина ничего не знает о HitTesting'e, поэтому мы должны позаботиться об этом самостоятельно. Иными словами, если бы мы не меняли видимость Джина, пользователь все так же мог бы кликнуть мышью по пустой области и попасть на невидимую кнопку спрятавшегося Джина.

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


Создание эффектов при помощи шейдеров


Третий Silverlight ввел поддержку эффектов, которые сыздавна были доступны разработчикам WPF-приложений. Теперь мы можем добавить любому  UIElement'у эффект тени (DropShadowEffect), или сделать его размытым (BlurEffect). Но как быть, если мы хотим создать свой собственный эффект, например, полностью убирающий красную составляющую из UIElement'a к которому он прикреплен? На помощь приходит класс ShaderEffect, который также был доступен WPF разработчикам с выходом WPF 3.5 SP1.

Суть ShaderEffect'a проста: взять визуальное представление элемента и прогнать его через пиксельный шейдер. Мое наивное определение пиксельного шейдера: это функция, возвращающая цвет точки в заданной координате. Правда, функция эта описывается не на C#, а на языке шейдеров, и исполняется обычно не на CPU а на GPU. Мы пользуемся С-подобным языком от Microsfot'a: HLSL (High Level Shaders Language - высокоуровневым языком шейдеров) для описания шейдера. В Silvelright 3 Beta 1 сам шейдер выполняется центральным процессором, не смотря на введенную поддержку GPU. В релиз версию парни из Майкрософт обещают включить поддержку выполнения шейдеров видео картой.

Алгоритм создания ShaderEffect'a:

1. Создаем файл с названием RemoveRed.fx. В файле определяем функцию шейдера:
float r : register(C0);

sampler2D implicitInputSampler : register(S0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
   float4 c = tex2D( implicitInputSampler, uv );
   c.r = r;
   return c;
} 
Здесь мы определили две глобальные переменные (r типа float; implicitInputSampler типа sampler2D) и одну функцию main(). На входе функция принимает переменную типа float2 - двумерный вектор, представляющий координаты точки, для которой требуется вычислить цвет. Выходом из функции служит переменная типа float4 - вектор с четырьмя измерениями, описывающий цвет точки (r, g, b, a). Значения координаты точки меняется в пределах от 0 до 1. Точка с координатами (0,0) - верхний левый угол, (1, 1) - нижний правый. Аналогичным диапазоном измеряется и составляющие цвета: от нуля до единицы.

Переменная типа sampler2D предоставляет механизм доступа к цветам элемента, к которому мы применяем эффект. Для того чтобы узнать какого цвета точка, скажем в правом верхнем углу достаточно вызвать встроенную функцию tex2D(implicitInputSampler, (1, 0)), которая вернет нам вектор со значениями цвета: (r, g, b, a). В примере выше, мы просто получаем истинный цвет точки в своей собственной позиции и изменяем красную составляющую на значение глобальной переменной r. Так, если значение r будет равно нулю мы получим следующее преобразование:


Значения глобальных переменных мы можем установить извне шейдера. Как именно - описано ниже. На этом мы закончим обзор HLSL и двинемся дальше, но пытливый читатель всегда найдет много полезной и интересной информации в разделах MSDN'a, посвященных языку программирования шейдеров.

2. Далее, мы компилируем эффект при помощи утилиты fxc.exe из DirectX SDK.. Просто запускаем компилятор из командной строки:

   fxc.exe /T ps_2_0 /E main /Fo RemoveRed.ps RemoveRed.fx
Здесь мы явно указываем компилятору использовать вторую модель пиксельных шейдеров (/T ps_2_0), в качестве входа в программу шейдера служит функция main (/E main), и, ожидаемым результатом компиляции файла RemoveRed.fx является объектный файл RemoveRed.ps (/Fo RemoveRed.ps RemoveRed.fx). Если у вас нет желания устанавливать дополнительно ПО, всегда можно воспользоваться онлайн компилятором:

3. Добавляем скомпилированный файл шейдеров в наш проект. Устанавливаем его Build Action в Resource:


4.Теперь создаем C# класс, который будет представлять наш шейдер в качестве эффекта:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

namespace SilverShaderExample
{
 public class RemoveRedEffect : ShaderEffect
 {
  public static DependencyProperty InputProperty =
   RegisterPixelShaderSamplerProperty("Input",
                                      typeof(RemoveRedEffect), 0);

  public static DependencyProperty RProperty =
   DependencyProperty.Register("R",
                               typeof(double),
                               typeof(RemoveRedEffect),
                               new PropertyMetadata(new double(), PixelShaderConstantCallback(0))
    );

  public RemoveRedEffect()
  {
   var u = new Uri(@"SilverShaderExample;Component/RemoveRed.ps", UriKind.Relative);
   PixelShader = new PixelShader { UriSource = u };

   // Инициализируем свойства, чтобы они получили верные начальные значения.
   UpdateShaderValue(InputProperty);
   UpdateShaderValue(RProperty);
  }

  public virtual Brush Input
  {
   get
   {
    return ((Brush)(GetValue(InputProperty)));
   }
   set
   {
    SetValue(InputProperty, value);
   }
  }

  public virtual double R
  {
   get
   {
    return ((double)(GetValue(RProperty)));
   }
   set
   {
    SetValue(RProperty, value);
   }
  }
 }
}
  

Здесь основной акцент стоит сделать на конструкторе класса и на регистрации Dependency свойств.

Давайте на секунду обратимся к .fx файлу. Помните наши глобальные переменные в шейдере? Так вот, при помощи Dependency-свойств, именно здесь происходит связывание переменных шейдера со свойствами managed-объекта. Сэмплер регистрируется при помощи метода RegisterPixelShaderSamplerProperty(), а переменная R использует PixelShaderConstantCallback() в качестве метода, реагирующего на изменения.

В конструкторе класса мы создаем ссылку на скомпилированный шейдер. Стоит быть очень внимательным в формировании этой ссылки. Например, если вы укажете Uri("RemoveRed.ps", UriKind.Relative) вместо Uri(@"SilverShaderExample;Component/RemoveRed.ps", UriKind.Relative), то сильверлайт упадет с далеко не очевидным объяснением исключения. Затем  мы создаем новый экземпляр класса PixelShader, который представляет собой управляемую обертку над нашим шейдером.

Поскольку переменные шейдера и managed-объекта связываются при помощи dependency-свойств, мы можем делать с переменными шейдерами все тоже, что и с обычными dependency-свойствами (анимировать, привязывать к данным, использовать в триггерах и т.д.).

Дальше дело за малым: применить эффект можно к любому UIElement'у. Так, следующий код, определяющий Grid с красным фоном будет отображен как черный прямоугольник, благодаря нашему  эффекту:

RemoveRed
В Silverlight 3.0 beta 1 можно применить только один эффект к одному элементу. Но элемент всегда можно вложить в несколько, скажем, Border'ов, и применить по одному из эффектов к каждому Border'у :).

Завершая наш обзор шейдерных эффектов, привожу ссылку на инструмент, значительно упрощающий работу: Shazzam Tool (click-once installer), Shazzam Help (Silverlight-based help). Shazzam позволяет отредактировать HLSL код:



Скомпилировать HLSL-код, посмотреть на результат и на сгенерированный C#- или VB-код для эффекта, не отходя от кассы (WYSIWYG, по-нашему):


Более того, если эффект содержит входящие параметры, их можно модифицировать прямо из Shazzam'a:

Вобщем, must-have, если вы серьезно настроены на создание своих эффектов :).

Алгоритм Genie


Здесь мы посмотрим как заставить Джина "свернуться" вниз. Сворачивание вверх, влево и вправо получается аналогично.

В основе эффекта лежит простая идея: за время t нужно свернуть картинку в точку (а точнее, в отрезок). При t = 0 картинка полностью развернута, а при t = 1 - полностью свернута. Как вы помните, функция пиксельного шейдера возвращает значение цвета точки. Т.е.нам нужно определить цвет заданной точки, с учетом текущего времени t. Это приводит нас к следующему коду:

float t : register(c0); // Время

sampler2D implicitInput : register(s0); // сэмплер

float4 main(float2 uv : TEXCOORD) : COLOR
{
 // [.. вычисление x1, y1 x2, y2 ..]
 
 if (uv.x < x1 || uv.x > x2 || uv.y < y1 || uv.y >y2)
 {
  return float4(0, 0, 0, 0);
 }
  
 float2 pos = float2((uv.x - x1)/(x2 - x1), (uv.y - y1)/(y2 - y1));
  
 return tex2D(implicitInput, pos);
}

Мы проверяем, или выходит текущая точка uv за пределы области (x1, y1, x2, y2). Если да - возвращаем прозрачный цвет (r = 0, g = 0, b = 0, a = 0), если же нет, то мы должны вернуть такой цвет, чтобы создался эффект "сжатия". Для этого мы отображаем ограничивающую нас область на всю картинку, т.е. (x1, x2) -> (0, 1), и (y1, y2) -> (0, 1). Что дает нам необходимые координаты: x = (uv.x - x1)/(x2 - x1), y = (uv.y - y1)/(y2 - y1).

Осталось посчитать x1, x2, y1, y2. Начальные значения переменных устанавливаются таким образом, чтобы вся картинка была видимой: x1 = 0, y1 = 0, x2 = 1, y2 = 1.Теперь мы вводим зависимость этих переменных от времени. Если, например, y1 будет увеличиваться пропорционально времени t: y1 = t, мы получим сжатие по вертикали:


Устанавливая более сложные зависимости, мы получаем Genie-эффект. Эффект состоит из двух фаз. Фаза сжатия по оси Х длится 40% времени:

Фаза сжатия по оси Y (оставшиеся 60%):

Для вычисления x1 и x2  я воспользовался кубическими кривыми Бизье. Одна кривая определяется четырьмя опорными точками P0, P1, P2 и P3. Кривая начинается в P0 и заканчивается в P3. Внутренние точки P1 и P2 управляют формой кривой и определяют начальный и конечный векторы касательных. Другими словами, кривая Бизье третьего порядка начинается в точке P0 направляясь к точке P1 и заканчивается в P3, подходя к ней со стороны точки P2:

Для нас началом кривой всегда является P0 = 0, а конец зависит от времени t, центра лампы и ширины ее "горлышка".  С учетом двухфазового подхода к эффекту, мы получаем следующий HLSL код:
float t : register(c0); // Время
float lampPosition : register(c1); // Центр Лампы
float lampWidth: register(c2); // Ширина Горлышка Лампы

sampler2D implicitInput : register(s0); // Сэмплер

// Параметрическая запись кубической кривой Бизье
// честно стибрена с http://ru.wikipedia.org/wiki/Кривая_Безье
float BezierInterpolate(float4 cp, float t)
{
     float b0 = pow(1-t, 3);
     float b1 = 3*t*pow(1-t,2);
     float b2 = 3*t*t*(1-t);
     float b3 = pow(t, 3);
     return b0*cp[0] + b1*cp[1] + b2*cp[2] + b3*cp[3];
}

float4 main(float2 uv : TEXCOORD) : COLOR
{
     float x1=0, x2=1, y1=0, y2 = 1;
     float4 lp, rp; // Опорные точки левой и правой кривых Бизье
     
     // Фаза сжатия по оси Y
     if (t > 0.4) 
     {
      lp[3] = lampPosition - lampWidth/2;
      rp[3] = lampPosition + lampWidth/2;
      y1 = (5*t - 2)/3.0;
     }
     else // Сжимаемся по оси X.
     {
      lp[3] = t*(lampPosition - lampWidth/2)/0.4;
      rp[3] = 1 + t*(lampPosition + lampWidth/2 - 1.0)/0.4;
      y1 = 0;
     }
     
     // Подсчитываем значения осташихся опорных точек
     // кривых Бизье.
     lp[2] = 0.90*lp[3];
     lp[1] = 0.25*lp[3];
     lp[0] = 0;
     
     rp[2] = 1.25*rp[3];
     rp[1] = 1-(rp[3] - rp[2] - lampWidth/2);
     rp[0] = 1;
     
     lp = saturate(lp); 
     rp = saturate(rp);

     // Вычисляем значение границ по оси X.
     x1 = BezierInterpolate(lp, uv.y);
     x2 = BezierInterpolate(rp, uv.y);
     
     if (uv.x < x1 || uv.x > x2 || uv.y < y1 || uv.y >y2)
     {
          return float4(0, 0, 0, 0);
     }
     
     float2 pos = float2((uv.x - x1)/(x2 - x1), (uv.y - y1)/(y2 - y1));
     
     return tex2D(implicitInput, pos);
}

Единственной темной лошадкой остался вызов HLSL-функции saturate(), применительно к опорным точкам кривых Бизье. Суть этого метода в отбрасывании точек за границей отрезка [0, 1]:
function saturate(x)
{
  if (x < 0) return 0;
  if (x > 1) return 1;
  return x;
}

Очень удобно, для проверки выхода за допустимые границы.

Вот, собственно, и вся магия :).


Что Дальше?


Итак, вы дочитали до самого конца. Спасибо, мне очень приятно :)! Напоследок могу предложить пару ресурсов, продолжающих тему эффектов и шейдеров:

Код к статье можно скачать здесь. Буду рад вашим комментариям.

Отличного настроения :)!

вторник, 7 апреля 2009 г.

UX Patterns

Привет!

Наткнулся на очень интересное собрание UX Pattern'ов (user experience patterns), от Infragistics. Сайт написан на Silverlight'e, что не может не радовать. Содержит детальное описание для каждого паттерна (в чем проблема, какое решение, в каком контексте используется, примеры). Можно так же смотреть связанные паттерны.

Вот несколько скринов (кликабельно):



Более того, можно предлагать свои находки. Молодцы, Infragistics. Молодцы!