WPF Как создать красивую волну букв - PullRequest
7 голосов
/ 24 декабря 2009

Мне нужно создать волнообразно выглядящий текстовый объект в моем приложении WPF, и я на самом деле предполагал, что будут варианты типа «изгиб по пути», но я совсем не вижу его в Blend.

Я нашел учебник, который предлагает вам преобразовать текст в букву пути за буквой, а затем повернуть его, но, на мой взгляд, это совершенно ужасно, так как в нем много места для ошибок и недостаточно гибкости.

Я хочу, чтобы предложение имело эффект анимированной волны, как мне этого добиться?

Спасибо всем Mark

Ответы [ 3 ]

36 голосов
/ 24 декабря 2009

То, что вы ищете, по сути является нелинейным преобразованием. Свойство Transform в Visual может выполнять только линейные преобразования. К счастью, 3D-функции WPF вам в помощь. Вы можете легко выполнить то, что вы ищете, создав простой пользовательский элемент управления, который будет использоваться следующим образом:

<local:DisplayOnPath Path="{Binding ...}" Content="Text to display" />

Вот как это сделать:

Сначала создайте пользовательский элемент управления «DisplayOnPath».

  1. Создайте его, используя пользовательский шаблон элемента управления Visual Studio (убедитесь, что ваша сборка: атрибут ThemeInfo установлен правильно и все такое)
  2. Добавить свойство зависимости «Путь» типа Geometry (используйте фрагмент wpfdp)
  3. Добавить свойство зависимостей «DisplayMesh» только для чтения типа Geometry3D (используйте фрагмент wpfdpro)
  4. Добавьте PropertyChangedCallback для Path, чтобы вызвать метод "ComputeDisplayMesh" для преобразования Path в Geometry3D, а затем установите DisplayMesh из него

Это будет выглядеть примерно так:

public class DisplayOnPath : ContentControl
{
  static DisplayOnPath()
  {
    DefaultStyleKeyProperty.OverrideMetadata ...
  }

  public Geometry Path { get { return (Geometry)GetValue(PathProperty) ...
  public static DependencyProperty PathProperty = ...  new UIElementMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      var displayOnPath = obj as DisplayOnPath;
      displayOnPath.DisplayMesh = ComputeDisplayMesh(displayOnPath.Path);
  }));

  public Geometry3D DisplayMesh { get { ... } private set { ... } }
  private static DependencyPropertyKey DisplayMeshPropertyKey = ...
  public static DependencyProperty DisplayMeshProperty = ...
}

Затем создайте шаблон стиля и элемента управления в Themes/Generic.xaml (или включенном в него ResourceDictionary), как для любого пользовательского элемента управления. Шаблон будет иметь следующее содержимое:

<Style TargetType="{x:Type local:DisplayOnPath}">

  <Setter Property="Template">
    <Setter.Value>

      <ControlTemplate TargetType="{x:Type local:DisplayOnPath}">

        <Viewport3DVisual ...>

          <ModelVisual3D>
            <ModelVisual3D.Content>

              <GeometryModel3D Geometry="{Binding DisplayMesh, RelativeSource={RelativeSource TemplatedParent}}">
                <GeometryModel3D.Material>

                  <DiffuseMaterial>
                    <DiffuseMaterial.Brush>

                      <VisualBrush ...>
                        <VisualBrush.Visual>

                          <ContentPresenter />
                ...

Для этого отображается трехмерная модель, которая использует DisplayMesh для определения местоположения и использует содержимое элемента управления в качестве материала кисти.

Обратите внимание, что вам может потребоваться установить другие свойства в Viewport3DVisual и VisualBrush, чтобы макет работал так, как вы хотите, и для визуального растяжения контента соответствующим образом.

Все, что осталось, это функция "ComputeDisplayMesh". Это тривиальное отображение, если вы хотите, чтобы верхняя часть содержимого (отображаемые слова) была перпендикулярна определенному расстоянию от пути. Конечно, есть и другие алгоритмы, которые вы можете выбрать вместо этого, например, создать параллельный путь и использовать процентное расстояние вдоль каждого.

В любом случае основной алгоритм такой же:

  1. Преобразовать в PathGeometry, используя PathGeometry.CreateFromGeometry
  2. Выберите подходящее количество прямоугольников в вашей сетке 'n', используя эвристику по вашему выбору. Возможно, начнем с жесткого кодирования n = 50.
  3. Вычислите ваши Positions значения для всех углов прямоугольников. Есть n + 1 углов в верхней части и n + 1 углов в нижней части. Каждый нижний угол можно найти, позвонив по номеру PathGeometry.GetPointAtFractionOfLength. Это также возвращает касательную, поэтому легко найти и верхний угол.
  4. Вычислите ваш TriangleIndices. Это тривиально. Каждый прямоугольник будет состоять из двух треугольников, поэтому в каждом прямоугольнике будет шесть индексов.
  5. Вычислите ваш TextureCoordinates. Это еще более тривиально, потому что все они будут 0, 1 или i / n (где i - индекс прямоугольника).

Обратите внимание, что если вы используете фиксированное значение n, единственное, что вам когда-либо придется пересчитывать при изменении пути, это массив Posisions. Все остальное исправлено.

Вот как выглядит основная часть этого метода:

var pathGeometry = PathGeometry.CreateFromGeometry(path);
int n=50;

// Compute points in 2D
var positions = new List<Point>();
for(int i=0; i<=n; i++)
{
  Point point, tangent;
  pathGeometry.GetPointAtFractionOfLength((double)i/n, out point, out tangent);
  var perpendicular = new Vector(tangent.Y, -tangent.X);
  perpendicular.Normalize();


  positions.Add(point + perpendicular * height); // Top corner
  positions.Add(point); // Bottom corner
}
// Convert to 3D by adding 0 'Z' value
mesh.Positions = new Point3DCollection(from p in positions select new Point3D(p.X, p.Y, 0));

// Now compute the triangle indices, same way
for(int i=0; i<n; i++)
{
  // First triangle
  mesh.TriangleIndices.Add(i*2+0);  // Upper left
  mesh.TriangleIndices.Add(i*2+2);  // Upper right
  mesh.TriangleIndices.Add(i*2+1);  // Lower left

  // Second triangle
  mesh.TriangleIndices.Add(i*2+1);  // Lower left
  mesh.TriangleIndices.Add(i*2+2);  // Upper right
  mesh.TriangleIndices.Add(i*2+3);  // Lower right
}
// Add code here to create the TextureCoordinates

Вот и все. Большая часть кода написана выше. Я оставляю это вам, чтобы заполнить все остальное.

Кстати, обратите внимание, что, проявив творческий подход со значением 'Z', вы можете получить действительно потрясающие эффекты.

Update

Марк реализовал код для этого и столкнулся с тремя проблемами. Вот проблемы и решения для них:

  1. Я допустил ошибку в своем порядке TriangleIndices для треугольника # 1. Это исправлено выше. Первоначально у меня были эти индексы, идущие вверху слева - внизу слева - вверху справа. Обойдя треугольник против часовой стрелки, мы фактически увидели заднюю часть треугольника, поэтому ничего не было нарисовано. Просто изменяя порядок индексов, мы вращаемся по часовой стрелке, чтобы треугольник был виден.

  2. Привязка к GeometryModel3D изначально была TemplateBinding. Это не сработало, потому что TemplateBinding не обрабатывает обновления таким же образом. Замена на обычную привязку устранила проблему.

  3. Система координат для 3D - это + Y вверх, тогда как для 2D + Y - вниз, поэтому путь оказался перевернутым. Это может быть решено либо отрицанием Y в коде, либо добавлением RenderTransform к ViewPort3DVisual, как вы предпочитаете. Я лично предпочитаю RenderTransform, потому что он делает код ComputeDisplayMesh более читабельным.

Вот снимок кода Марка, оживляющего настроение, которое, я думаю, мы все разделяем:

Snapshot of animating text
(источник: rayburnsresume.com )

8 голосов
/ 18 января 2010

Возможно, вы захотите ознакомиться со статьей Чарльза Петцольда в MSDN Отображение текста по пути с WPF ( архивная версия здесь ).

wavy text

Я нашел эту статью очень полезной, и он также предоставляет пример, где он использует анимацию.

0 голосов
/ 31 декабря 2009

Я думал, что на самом деле опубликую подробности моего прогресса, чтобы мы могли выбраться из комментариев (которые не форматируются как хорошие :))

Вот мое главное окно:

<Window.Resources>
        <Style TargetType="{x:Type local:DisplayOnPath}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:DisplayOnPath}">
                        <Viewport3D>
                            <Viewport3D.Camera>
                                <PerspectiveCamera FieldOfView="60" 
                                               FarPlaneDistance="1000" 
                                               NearPlaneDistance="10" 
                                               Position="0,0,300" 
                                               LookDirection="0,0,-1" 
                                               UpDirection="0,1,0"/>
                            </Viewport3D.Camera>
                            <ModelVisual3D>
                                <ModelVisual3D.Content>
                                    <Model3DGroup>
                                        <AmbientLight Color="#ffffff" />
                                        <GeometryModel3D Geometry="{TemplateBinding DisplayMesh}">
                                            <GeometryModel3D.Material>
                                                <DiffuseMaterial>
                                                    <DiffuseMaterial.Brush>
                                                        <SolidColorBrush Color="Red" />
                                                    </DiffuseMaterial.Brush>
                                                </DiffuseMaterial>
                                            </GeometryModel3D.Material>
                                        </GeometryModel3D>
                                    </Model3DGroup>
                                </ModelVisual3D.Content>
                            </ModelVisual3D>
                        </Viewport3D>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Storyboard x:Key="movepath">
            <PointAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="p1" Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[4].(LineSegment.Point)">
                <SplinePointKeyFrame KeyTime="00:00:01" Value="181.5,81.5"/>
            </PointAnimationUsingKeyFrames>
            <PointAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="p1" Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[3].(LineSegment.Point)">
                <SplinePointKeyFrame KeyTime="00:00:01" Value="141.5,69.5"/>
            </PointAnimationUsingKeyFrames>
            <PointAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="p1" Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[1].(LineSegment.Point)">
                <SplinePointKeyFrame KeyTime="00:00:01" Value="62.5,49.5"/>
            </PointAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>

    <Window.Triggers>
        <EventTrigger RoutedEvent="FrameworkElement.Loaded">
            <BeginStoryboard Storyboard="{StaticResource movepath}"/>
        </EventTrigger>
    </Window.Triggers>

  <Grid x:Name="grid1">
    <Path x:Name="p1" Stroke="Black" Margin="238.5,156.5,331.5,0" VerticalAlignment="Top" Height="82">
        <Path.Data>
            <PathGeometry>
                <PathFigure StartPoint="0.5,0.5">
                    <LineSegment Point="44.5,15.5"/>
                    <LineSegment Point="73.5,30.5"/>
                    <LineSegment Point="91.5,56.5"/>
                    <LineSegment Point="139.5,53.5"/>
                    <LineSegment Point="161,80"/>
                </PathFigure>
            </PathGeometry>
        </Path.Data>
    </Path>
    <local:DisplayOnPath x:Name="wave1" Path="{Binding Data, ElementName=p1, Mode=Default}" />
    </Grid>

Тогда у меня есть фактический пользовательский контроль:

public partial class DisplayOnPath : UserControl
    {
        public MeshGeometry3D DisplayMesh
        {
            get { return (MeshGeometry3D)GetValue(DisplayMeshProperty); }
            set { SetValue(DisplayMeshProperty, value); }
        }

        public Geometry Path
        {
            get { return (Geometry)GetValue(PathProperty); }
            set { SetValue(PathProperty, value); }
        }

        public static readonly DependencyProperty DisplayMeshProperty = 
            DependencyProperty.Register("DisplayMesh", typeof(MeshGeometry3D), typeof(DisplayOnPath), new FrameworkPropertyMetadata(new MeshGeometry3D(), FrameworkPropertyMetadataOptions.AffectsRender));

        public static readonly DependencyProperty PathProperty =
        DependencyProperty.Register("Path", 
                                    typeof(Geometry), 
                                    typeof(DisplayOnPath), 
                                    new PropertyMetadata()
                                    {
                                        PropertyChangedCallback = (obj, e) =>
                                        {
                                            var displayOnPath = obj as DisplayOnPath;
                                            displayOnPath.DisplayMesh = ComputeDisplayMesh(displayOnPath.Path);
                                        }
                                    }
        );

        private static MeshGeometry3D ComputeDisplayMesh(Geometry path)
        {
            var mesh = new MeshGeometry3D();

            var pathGeometry = PathGeometry.CreateFromGeometry(path);
            int n = 50;
            int height = 10;

            // Compute points in 2D
            var positions = new List<Point>();
            for (int i = 0; i <= n; i++)
            {
                Point point, tangent;
                pathGeometry.GetPointAtFractionLength((double)i / n, out point, out tangent);
                var perpendicular = new Vector(tangent.Y, -tangent.X);
                perpendicular.Normalize();
                positions.Add(point + perpendicular * height); // Top corner
                positions.Add(point); // Bottom corner
            }
            // Convert to 3D by adding 0 'Z' value
            mesh.Positions = new Point3DCollection(from p in positions select new Point3D(p.X, p.Y, 0));

            // Now compute the triangle indices, same way
            for (int i = 0; i < n; i++)
            {
                // First triangle
                mesh.TriangleIndices.Add(i * 2 + 0);  // Upper left
                mesh.TriangleIndices.Add(i * 2 + 1);  // Lower left
                mesh.TriangleIndices.Add(i * 2 + 2);  // Upper right
                // Second triangle
                mesh.TriangleIndices.Add(i * 2 + 1);  // Lower left
                mesh.TriangleIndices.Add(i * 2 + 2);  // Upper right
                mesh.TriangleIndices.Add(i * 2 + 3);  // Lower right
            }

            for (int i = 0; i <= n; i++)
            {
                for (int j = 0; j < 2; j++)
                {
                    mesh.TextureCoordinates.Add(new Point((double) i/n, j));
                }
            }

            //Console.WriteLine("Positions=\"" + mesh.Positions + "\"\nTriangleIndices=\"" + mesh.TriangleIndices +
            //                  "\"\nTextureCoordinates=\"" + mesh.TextureCoordinates + "\"");
            return mesh;
        }

        static DisplayOnPath()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DisplayOnPath), new FrameworkPropertyMetadata(typeof(DisplayOnPath)));
        }

        public DisplayOnPath()
        {
            InitializeComponent();
        }
    }

На данный момент, как есть, это не делает ничего, кроме пути.

НО, если вы получите детали сетки wave1 после загрузки окна, затем замените привязку на жестко закодированные значения, вы получите это: http://img199.yfrog.com/i/path1.png/

У которого есть 2 основные проблемы:

  1. Треугольники все заостренные, поэтому я думаю, что прямоугольники определены неправильно
  2. Все наоборот! Но я думаю, что это как-то связано с касательными
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...