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

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

tag cloud

О чем это мы тут?

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

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

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

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

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

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

XAML

<UserControl x:Class="Mercury.Web.Silverlight.Tags.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <Grid x:Name="LayoutRoot" Background="White">
        <Canvas x:Name="RootCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        </Canvas>
    </Grid>
</UserControl>

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

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

public class Tag3D
{
    public Tag3D(double x, double y, double z, string text, Uri uri)
    {
        CenterPoint = new Point3D(x, y, z);
        TextBlock = new TextBlock { Text = text };
        BtnLink = new HyperlinkButton
                  {
                      Content = TextBlock,
                      BorderThickness = new Thickness(0),
                      Padding = new Thickness(0),
                      NavigateUri = uri,
                      Visibility = Visibility.Collapsed
                  };
    }

    public HyperlinkButton BtnLink { get; set; }
    public Point3D CenterPoint { get; set; }
    private TextBlock TextBlock { get; set; }

    public void Redraw(UISize size) { ... }
    private void UpdatePosition(double rx, double ry, double xOffset, double yOffset) { ... }
    private void UpdateLayout() { ... }
    private void UpdateSize() { ... }
    private void UpdateColor() { ... }
    private void UpdateVisibility() { ... }
}

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

    private void UpdateSize()
    {
        BtnLink.FontSize = 16 + CenterPoint.Z * 4;
    }

    private void UpdateColor()
    {
        var color = 160 + CenterPoint.Z * 96;
        color = Math.Max(0, color);
        color = Math.Min(255, color);
        BtnLink.Foreground = new SolidColorBrush(Color.FromArgb(Convert.ToByte(color), 0, 0, 0));
    }

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

    // rx, ry - размеры эллипса облака (в проекции на плоскость)
    // xOffset, yOffset - координаты центра облака, относительно Canvas
    private void UpdatePosition(double rx, double ry, double xOffset, double yOffset)
    {
        var maxZ = Math.Min(rx, ry);
        var x = (xOffset + CenterPoint.X * rx) - (BtnLink.ActualWidth / 2.0);
        var y = (yOffset - CenterPoint.Y * ry) - (BtnLink.ActualHeight / 2.0);
        var z = CenterPoint.Z * maxZ;

        Canvas.SetLeft(BtnLink, x);
        Canvas.SetTop(BtnLink, y);
        Canvas.SetZIndex(BtnLink, Convert.ToInt32(z));
    }

Размещение тегов на поверхности сферы: Вернемся к классу MainPage.

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

    private void FillTags()
    {
        RootCanvas.UpdateLayout();
        RootCanvas.Children.Clear();
        tagBlocks = new List<Tag3D>();

        var length = tagList.Count;
        for (var i = 1; i <= length; i++)
        {
            var phi = Math.Acos(-1.0 + (2.0 * i - 1.0) / length);
            var theta = Math.Sqrt(length Math.PI) phi;
            var x = Math.Cos(theta) * Math.Sin(phi);
            var y = Math.Sin(theta) * Math.Sin(phi);
            var z = Math.Cos(phi);

            var tag = tagList[i - 1];
            var uri = UrlHelper.GetTagUri(UrlEncode(tag));

            var item = new Tag3D(x, y, z, tag, uri);
            RootCanvas.Children.Add(item.BtnLink);
            tagBlocks.Add(item);
        }
    }

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

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

    private readonly RotateTransform3D rotateTransform = new RotateTransform3D();

    public void Run()
    {
        CompositionTarget.Rendering += OnCompositionTargetRendering;
        LayoutRoot.MouseEnter += OnLayoutRootMouseEnter;
        LayoutRoot.MouseLeave += OnLayoutRootMouseLeave;

        slowDownCounter = 500.0;
        runRotation = true;
        rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(0.8, 0.6, 0), 0.5);

        FillTags();
    }

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

    private void OnLayoutRootMouseMove(object sender, MouseEventArgs e)
    {
        var position = e.GetPosition(RootCanvas);
        SetRotateTransform(position);
    }

    private void SetRotateTransform(Point position)
    {
        var size = GetUISizes();

        var x = (position.X - size.XOffset) / size.XRadius;
        var y = (position.Y - size.YOffset) / size.YRadius;
        var angle = Math.Sqrt(x x + y y);
        rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(-y, -x, 0.0), angle);
    }

    private UISize GetUISizes()
    {
        return new UISize
               {
                   XOffset = RootCanvas.ActualWidth / 2.0,
                   YOffset = RootCanvas.ActualHeight / 2.0
               };
    }

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

    private void OnCompositionTargetRendering(object sender, EventArgs e)
    {
        if (!(runRotation || (slowDownCounter <= 0.0)))
        {
            var rotation = (AxisAngleRotation3D)rotateTransform.Rotation;
            rotation.Angle *= slowDownCounter / 500.0;
            rotateTransform.Rotation = rotation;
            slowDownCounter--;
        }
        if (((AxisAngleRotation3D)rotateTransform.Rotation).Angle > 0.05)
        {
            RotateBlocks();
        }
    }

    private void RotateBlocks()
    {
        var size = GetUISizes();

        foreach (var tagd in tagBlocks)
        {
            Point3D pointd;
            if (rotateTransform.TryTransform(tagd.CenterPoint, out pointd))
            {
                tagd.CenterPoint = pointd;
                tagd.Redraw(size);
            }
        }
    }

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

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

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

<?xml version="1.0" encoding="utf-8"?>
<taglist>
    <tag>IIS7</tag>
    <tag>.NET</tag>
    <tag>Silverlihgt</tag>
    <tag>3D</tag>
</taglist>

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

    private void LoadTagList(Uri uri)
    {
        try
        {
            // Получаем XML со списком тегов.
            var request = (HttpWebRequest)WebRequest.Create(uri);

            var waitingEvent = new AutoResetEvent(false);
            var callback = new AsyncCallback(result => ((EventWaitHandle)result.AsyncState).Set());

            var asyncResult = request.BeginGetResponse(callback, waitingEvent);
            waitingEvent.WaitOne();

            var response = request.EndGetResponse(asyncResult);

            // Парсим XML и составляем список тегов.
            var doc = XDocument.Load(response.GetResponseStream());
            tagList.AddRange(doc.Descendants("tag").Select(item => item.Value));

            Dispatcher.BeginInvoke(Run);
        }
        catch
        {
        }
    }

Все!

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

HTML код:

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

Вот и все!