2009-12-23 2 views
7

Tengo la necesidad de crear un objeto de texto wavey buscando en mi aplicación WPF, y en realidad estaba suponiendo que habría un tipo de opciones de "doblar a lo largo de una ruta", pero no veo ninguno en Blend.WPF ¿Cómo se puede crear una bonita ola de letras buscando

Encontré un tutorial que sugiere que necesitas convertir tu texto a una letra de ruta por letra y luego rotarlo, pero eso es totalmente horrible en mi opinión, a mucho margen de error y poca flexibilidad.

Básicamente quiero una oración para tener un efecto de onda animada, ¿cómo puedo lograrlo?

Gracias a todos Marcos

Respuesta

8

Es posible que desee comprobar hacia fuera el artículo de MSDN de Charles Petzold Render Text On A Path With WPF (archived version here).

wavy text

He encontrado este artículo muy útil y que también proporciona una muestra donde se utiliza la animación.

+0

gracias por el enlace, no puedo creer que este artículo no apareció en mi Google ... – Mark

+0

sin preocupaciones - contento Yo podría ayudar :-) –

36

Lo que estamos buscando es efectivamente una transformada no lineal. La propiedad Transform en Visual solo puede hacer transformaciones lineales. Afortunadamente, las características 3D de WPF vienen a tu rescate. Esto se puede hacer fácilmente lo que busca mediante la creación de un control personalizado simple que sería utilizado como esto:

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

Aquí es cómo hacerlo:

En primer lugar crear el control personalizado "DisplayOnPath".

  1. crear utilizando la plantilla de control personalizado de Visual Studio (asegurándose de que su montaje: ThemeInfo atributo se establece correctamente y todo eso)
  2. Añadir una propiedad de dependencia "trayectoria" de tipo Geometry (uso wpfdp fragmento de código)
  3. Agregar una dependencia de sólo lectura propiedad "DisplayMesh" del tipo Geometry3D (uso wpfdpro fragmento de código)
  4. Añadir un PropertyChangedCallback para la Ruta de llamar a un método "ComputeDisplayMesh" para convertir la Ruta a un Geometry3D, a continuación, establecer DisplayMesh de ella

Se verá algo como esto:

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 = ... 
} 

A continuación, cree la plantilla de estilo y control en Themes/Generic.xaml (o una ResourceDictionary incluido por ella) que para cualquier control personalizado. La plantilla tendrá contenidos como esto:

<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 /> 
       ... 

Lo que esto hace es mostrar un modelo 3D que utiliza un DisplayMesh por su ubicación y utiliza el contenido de su control como un material de cepillo.

Tenga en cuenta que puede necesitar establecer otras propiedades en Viewport3DVisual y VisualBrush para que el diseño funcione de la manera que desee y para que la visualización de contenido se extienda de forma adecuada.

Todo lo que queda es la función "ComputeDisplayMesh". Esta es una asignación trivial si desea que la parte superior del contenido (las palabras que está visualizando) sea perpendicular a cierta distancia de la ruta. Por supuesto, hay otros algoritmos que puede elegir, como crear una ruta paralela y usar la distancia porcentual a lo largo de cada uno.

En cualquier caso, el algoritmo básico es el mismo:

  1. Convertir a PathGeometry usando PathGeometry.CreateFromGeometry
  2. Seleccionar un número apropiado de rectángulos en su malla, 'n', usando una heurística de su elección. Tal vez comience con codificación dura n = 50.
  3. Calcule sus valores Positions para todas las esquinas de los rectángulos. Hay n + 1 esquinas en la parte superior y n + 1 esquinas en la parte inferior. Cada esquina inferior se puede encontrar llamando al PathGeometry.GetPointAtFractionOfLength. Esto también devuelve una tangente, por lo que es fácil encontrar la esquina superior también.
  4. Calcula tu TriangleIndices. Esto es trivial. Cada rectángulo será de dos triángulos, por lo que habrá seis índices por rectángulo.
  5. Calcule su TextureCoordinates. Esto es aún más trivial, porque todos serán 0, 1 o i/n (donde i es el índice del rectángulo).

Tenga en cuenta que si está utilizando un valor fijo de n, lo único que tendrá que volver a calcular cuando la ruta cambie es la matriz Posisions. Todo lo demás está arreglado.

Aquí es el lo que la parte principal de este método es así:

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 

Eso es todo. La mayor parte del código está escrito arriba. Te dejo completar el resto.

Por cierto, tenga en cuenta que al ser creativo con el valor 'Z', puede obtener algunos efectos verdaderamente sorprendentes.

actualización

Marcos implementado el código para esto y se encontró con tres problemas. Aquí están los problemas y las soluciones para ellos:

  1. Cometí un error en mi orden TriangleIndices para el triángulo # 1. Se corrigió arriba. Originalmente tenía esos índices yendo arriba a la izquierda - abajo a la izquierda - arriba a la derecha. Al rodear el triángulo en sentido antihorario, vimos la parte posterior del triángulo, por lo que no se pintó nada. Simplemente cambiando el orden de los índices vamos alrededor de las agujas del reloj para que el triángulo sea visible.

  2. El enlace en el GeometryModel3D era originalmente un TemplateBinding. Esto no funcionó porque TemplateBinding no maneja las actualizaciones de la misma manera. Cambiarlo a un enlace regular solucionó el problema.

  3. El sistema de coordenadas para 3D es + Y está arriba, mientras que para 2D + Y está abajo, por lo que la ruta apareció boca abajo. Esto se puede resolver anulando Y en el código o agregando un RenderTransform en el ViewPort3DVisual, como prefiera. Personalmente prefiero RenderTransform porque hace que el código ComputeDisplayMesh sea más legible.

Aquí es una instantánea de código de Mark animación de un sentimiento que creo que todos compartimos:

Snapshot of animating text "StackOverflowIsFun" http://rayburnsresume.com/StackOverflowImages/WavyLetters.png

+6

+1 por la gran cantidad de esfuerzo que pones en tu respuesta. ¡Prestigio! –

+4

+1. No entendí la mitad de tu respuesta, pero para una explicación tan completa, te lo mereces;) –

+1

Esa es una respuesta increíble, necesito algo de tiempo para digerir esto y entrar en él ... Me pondré en contacto contigo cuando He tenido la oportunidad de jugar En serio, muchas gracias por el esfuerzo, si puedo hacer que funcione como yo quiero, entonces esta será una gran victoria para mi proyecto, no tienes idea de lo emocionado que esto podría tener a la gente :) – Mark

0

pensé que en realidad publicar los detalles de mi progreso para que podamos salir de los comentarios (que dont formato tan bonito :))

Aquí está mi ventana principal:

<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> 

entonces tengo el control de usuario real:

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(); 
     } 
    } 

En el momento como es, esto no hace que no sea el camino nada.

pero si te dan los detalles de malla de wave1 después de la ventana se haya cargado, vuelva a colocar los valores a codificar duro vinculante, se obtiene lo siguiente: http://img199.yfrog.com/i/path1.png/

que tiene 2 problemas principales como es:

  1. Los triángulos son todos puntiagudos, por lo que creo que los rectángulos no se están definiendo correctamente
  2. Se invierte! Pero creo que eso tiene algo que ver con las tangentes
+1

debe editar su pregunta original, no agregar una respuesta (ya que esta no es una respuesta) –

Cuestiones relacionadas