Создание 3D облака тегов на Silverlight

.NET Silverlihgt 3D Облако тегов

 

tag cloud

В этом проекте я хотел бы рассказать о том, как можно сделать красивое облако тегов на Silverlight (т.е. такое облако, какое реализовано у меня на сайте).

Сразу хочу признаться, что идею и часть реализации я подсмотрел у других. Однако, приведенный там пример слегка глючил. Поэтому я решил написать данную статью.

Поставим себе задачу следующим образом:

  • отображение тегов на поверхности 3D сферы (или эллипсоида);
  • вращение облака в зависимости от положения мыши;
  • получение информации о тегах в XML документе.

Выбор 3D библиотеки:

К сожалению, последняя версия Silverlight не включает в себя трехмерные возможности как в WPF. К счастью, часть необходимого функционала написана сторонними разработчиками. В данном проекте будет используется библиотека Axelerate3D.

XAML:

  1. <UserControl x:Class="Mercury.Web.Silverlight.Tags.MainPage"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
  5.     <Grid x:Name="LayoutRoot" Background="White">
  6.         <Canvas x:Name="RootCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
  7.         </Canvas>
  8.     </Grid>
  9. </UserControl>

Тут приведен XAML-код основного класса Silverlight приложения (MainPage). Как видите, здесь все просто. Один единственный Canvas, который будет заниматься рендерингом облака.

Отображение тегов:

Перейдем к созданию класса работы с тегом. Он (класс) будет представлять метку с нужным текстом, которая будет расположена в координатах (x, y, z) относительно центра облака.

  1. public class Tag3D
  2. {
  3.     public Tag3D(double x, double y, double z, string text, Uri uri)
  4.     {
  5.         CenterPoint = new Point3D(x, y, z);
  6.         TextBlock = new TextBlock { Text = text };
  7.         BtnLink = new HyperlinkButton
  8.                   {
  9.                       Content = TextBlock,
  10.                       BorderThickness = new Thickness(0),
  11.                       Padding = new Thickness(0),
  12.                       NavigateUri = uri,
  13.                       Visibility = Visibility.Collapsed
  14.                   };
  15.     }
  16.  
  17.     public HyperlinkButton BtnLink { get; set; }
  18.     public Point3D CenterPoint { get; set; }
  19.     private TextBlock TextBlock { get; set; }
  20.  
  21.     public void Redraw(UISize size) { ... }
  22.     private void UpdatePosition(double rx, double ry, double xOffset, double yOffset) { ... }
  23.     private void UpdateLayout() { ... }
  24.     private void UpdateSize() { ... }
  25.     private void UpdateColor() { ... }
  26.     private void UpdateVisibility() { ... }
  27. }

Затем нам необходимо сделать методы для определения размеров и цвета выбранного тега:

  1.     private void UpdateSize()
  2.     {
  3.         BtnLink.FontSize = 16 + CenterPoint.Z * 4;
  4.     }
  5.  
  6.     private void UpdateColor()
  7.     {
  8.         var color = 160 + CenterPoint.Z * 96;
  9.         color = Math.Max(0, color);
  10.         color = Math.Min(255, color);
  11.         BtnLink.Foreground = new SolidColorBrush(Color.FromArgb(Convert.ToByte(color), 0, 0, 0));
  12.     }

Теперь позиционируем тег в пространстве:

  1.     // rx, ry - размеры эллипса облака (в проекции на плоскость)
  2.     // xOffset, yOffset - координаты центра облака, относительно Canvas
  3.     private void UpdatePosition(double rx, double ry, double xOffset, double yOffset)
  4.     {
  5.         var maxZ = Math.Min(rx, ry);
  6.         var x = (xOffset + CenterPoint.X * rx) - (BtnLink.ActualWidth / 2.0);
  7.         var y = (yOffset - CenterPoint.Y * ry) - (BtnLink.ActualHeight / 2.0);
  8.         var z = CenterPoint.Z * maxZ;
  9.  
  10.         Canvas.SetLeft(BtnLink, x);
  11.         Canvas.SetTop(BtnLink, y);
  12.         Canvas.SetZIndex(BtnLink, Convert.ToInt32(z));
  13.     }

Размещение тегов на поверхности сферы:

Вернемся к классу MainPage.

Для понимания следующего фрагмента кода необходимо некоторое знание математики. Этот метод распределяет теги по поверхности сферы.

  1.     private void FillTags()
  2.     {
  3.         RootCanvas.UpdateLayout();
  4.         RootCanvas.Children.Clear();
  5.         tagBlocks = new List<Tag3D>();
  6.  
  7.         var length = tagList.Count;
  8.         for (var i = 1; i <= length; i++)
  9.         {
  10.             var phi = Math.Acos(-1.0 + (2.0 * i - 1.0) / length);
  11.             var theta = Math.Sqrt(length * Math.PI) * phi;
  12.             var x = Math.Cos(theta) * Math.Sin(phi);
  13.             var y = Math.Sin(theta) * Math.Sin(phi);
  14.             var z = Math.Cos(phi);
  15.  
  16.             var tag = tagList[i - 1];
  17.             var uri = UrlHelper.GetTagUri(UrlEncode(tag));
  18.  
  19.             var item = new Tag3D(x, y, z, tag, uri);
  20.             RootCanvas.Children.Add(item.BtnLink);
  21.             tagBlocks.Add(item);
  22.         }
  23.     }

Вращение облака:

Для вращения облака мы будем использовать положение курсора мыши. Чем больше расстояние от центра облака, до курсора, тем больше угол поворота облака, а значит и скорость его вращения.

В момент инициализации придаем небольшое начальное вращение:

  1.     private readonly RotateTransform3D rotateTransform = new RotateTransform3D();
  2.  
  3.     public void Run()
  4.     {
  5.         CompositionTarget.Rendering += OnCompositionTargetRendering;
  6.         LayoutRoot.MouseEnter += OnLayoutRootMouseEnter;
  7.         LayoutRoot.MouseLeave += OnLayoutRootMouseLeave;
  8.  
  9.         slowDownCounter = 500.0;
  10.         runRotation = true;
  11.         rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(0.8, 0.6, 0), 0.5);
  12.  
  13.         FillTags();
  14.     }

Определяем направление и скорость вращения облака:

  1.     private void OnLayoutRootMouseMove(object sender, MouseEventArgs e)
  2.     {
  3.         var position = e.GetPosition(RootCanvas);
  4.         SetRotateTransform(position);
  5.     }
  6.  
  7.     private void SetRotateTransform(Point position)
  8.     {
  9.         var size = GetUISizes();
  10.  
  11.         var x = (position.X - size.XOffset) / size.XRadius;
  12.         var y = (position.Y - size.YOffset) / size.YRadius;
  13.         var angle = Math.Sqrt(x * x + y * y);
  14.         rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(-y, -x, 0.0), angle);
  15.     }
  16.  
  17.     private UISize GetUISizes()
  18.     {
  19.         return new UISize
  20.                {
  21.                    XOffset = RootCanvas.ActualWidth / 2.0,
  22.                    YOffset = RootCanvas.ActualHeight / 2.0
  23.                };
  24.     }

Теперь займемся отрисовкой тегов.

  1.     private void OnCompositionTargetRendering(object sender, EventArgs e)
  2.     {
  3.         if (!(runRotation || (slowDownCounter <= 0.0)))
  4.         {
  5.             var rotation = (AxisAngleRotation3D)rotateTransform.Rotation;
  6.             rotation.Angle *= slowDownCounter / 500.0;
  7.             rotateTransform.Rotation = rotation;
  8.             slowDownCounter--;
  9.         }
  10.         if (((AxisAngleRotation3D)rotateTransform.Rotation).Angle > 0.05)
  11.         {
  12.             RotateBlocks();
  13.         }
  14.     }
  15.  
  16.     private void RotateBlocks()
  17.     {
  18.         var size = GetUISizes();
  19.  
  20.         foreach (var tagd in tagBlocks)
  21.         {
  22.             Point3D pointd;
  23.             if (rotateTransform.TryTransform(tagd.CenterPoint, out pointd))
  24.             {
  25.                 tagd.CenterPoint = pointd;
  26.                 tagd.Redraw(size);
  27.             }
  28.         }
  29.     }

Загрузка списка тегов:

Теперь можно позаботиться о том, чтобы наполнить наше облака данными о тегах.

Путь некоторый Url возвращает нам XML вот такой структуры:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <taglist>
  3.     <tag>IIS7</tag>
  4.     <tag>.NET</tag>
  5.     <tag>Silverlihgt</tag>
  6.     <tag>3D</tag>
  7. </taglist>

Тогда код для получения списка тегов может выглядеть так:

  1.     private void LoadTagList(Uri uri)
  2.     {
  3.         try
  4.         {
  5.             // Получаем XML со списком тегов.
  6.             var request = (HttpWebRequest)WebRequest.Create(uri);
  7.  
  8.             var waitingEvent = new AutoResetEvent(false);
  9.             var callback = new AsyncCallback(result => ((EventWaitHandle)result.AsyncState).Set());
  10.  
  11.             var asyncResult = request.BeginGetResponse(callback, waitingEvent);
  12.             waitingEvent.WaitOne();
  13.  
  14.             var response = request.EndGetResponse(asyncResult);
  15.  
  16.             // Парсим XML и составляем список тегов.
  17.             var doc = XDocument.Load(response.GetResponseStream());
  18.             tagList.AddRange(doc.Descendants("tag").Select(item => item.Value));
  19.  
  20.             Dispatcher.BeginInvoke(Run);
  21.         }
  22.         catch
  23.         {
  24.         }
  25.     }

Все! 

Исходники можно загрузить здесь. 

HTML код: 

  1. <object width="500" height="300" type="application/x-silverlight-2" data="data:application/x-silverlight-2,">
  2.     <param name="source" value="/ClientBin/Mercury.Web.Silverlight.Tags.xap" />
  3.     <param name="background" value="white" />
  4.     <param name="minRuntimeVersion" value="3.0.40624.0" />
  5.     <param name="autoUpgrade" value="true" />
  6.     <param name="windowless" value="true" />
  7.     <param name="enableGPUAcceleration" value="True" />
  8.     <param name="initParams" value="url=/Node/Tags/{0},service=/Node/TagList" />
  9.     <a style="text-decoration: none" href="http://go.microsoft.com/fwlink/?LinkID=149156&amp;amp;v=3.0.40624.0">
  10.         <img style="border-style: none"
  11.             src="http://go.microsoft.com/fwlink/?LinkId=108181"
  12.             alt="Get Microsoft Silverlight" />
  13.     </a>
  14. </object>

Вот и все!

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.