WPF, Как можно создать симпатичную волну букв

У меня есть потребность создать wavey смотрящий текстовый объект в моем приложении WPF, и я на самом деле предполагал, что будет "изгиб вдоль пути" тип опций, но я не вижу один вообще в Смешении.

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

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

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

7
задан Mark 23 December 2009 в 23:10
поделиться

3 ответа

Вы, возможно, захотите ознакомиться со статьей Чарльза Петцольда по MSDN Render Text On A Path With WPF (архивная версия здесь).

wavy text

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

8
ответ дан 6 December 2019 в 04:56
поделиться

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

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

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

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

  1. Создайте его с помощью Шаблон настраиваемого элемента управления Visual Studio (убедитесь, что ваша сборка: атрибут ThemeInfo установлен правильно и все такое)
  2. Добавьте свойство зависимости «Path» типа 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 />
                ...

Он отображает 3D-модель, которая использует DisplayMesh для определения местоположения и использует Контент вашего элемента управления в качестве материала кисти.

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

Все, что осталось, это " GetPointAtFractionOfLength . Это также возвращает касательную, поэтому также легко найти верхний угол.

  • Вычислите свой TriangleIndices . Это банально. Каждый прямоугольник будет состоять из двух треугольников, поэтому на каждый прямоугольник будет шесть индексов.
  • Вычислите свои координаты текстуры . Это еще более тривиально, потому что все они будут равны 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", вы можете получить действительно потрясающие эффекты.

    Обновление

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

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

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

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

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

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

    36
    ответ дан 6 December 2019 в 04:56
    поделиться

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

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

    <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. Их положение перевернуто! Но я думаю, что это как-то связано с касательными
    0
    ответ дан 6 December 2019 в 04:56
    поделиться
    Другие вопросы по тегам:

    Похожие вопросы: