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

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>

Вот и все!

Комментарии

  • test#

    Подскажите, пожалуйста, текст вебслужбы.

  • Виталий Лещенкоавтор#

    Лисенка, к сожалению, я не представляю как это возможно реализовать средствами чистого CSS.

  • Виталий Лещенкоавтор#

    Для Татьяны: как таковая web служба у меня здесь не используется. Сайт написан на ASP.NET MVC.

    За формирование списка тегов отвечает урл: /Node/TagList

    View'шка предельно простая:

    <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<List<string>>" %><?xml version="1.0" encoding="utf-8"?> <taglist><% foreach (var item in Model) { %> <tag><%= Html.Encode(item) %></tag><% } %> </taglist>

    Как видно из текста вьюшки, моделью является список строк. Вес тега не используется, так как изменение размеров текста может нарушить восприятие сферы.

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

  • Виталий Лещенкоавтор#

    Для Vitaliy: Исходный код можно свободно скачать здесь.

  • test#

    Спасибо за подробное описание, однако нет ли реализации этого облака на чистом CSS?

  • test#

    Вишли мне плиз исходники на мейл: geruch_vitaliy@mail.ru. Пасиба

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

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