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".
- crear utilizando la plantilla de control personalizado de Visual Studio (asegurándose de que su montaje: ThemeInfo atributo se establece correctamente y todo eso)
- Añadir una propiedad de dependencia "trayectoria" de tipo
Geometry
(uso wpfdp fragmento de código)
- Agregar una dependencia de sólo lectura propiedad "DisplayMesh" del tipo
Geometry3D
(uso wpfdpro fragmento de código)
- 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:
- Convertir a
PathGeometry
usando PathGeometry.CreateFromGeometry
- 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.
- 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.
- Calcula tu
TriangleIndices
. Esto es trivial. Cada rectángulo será de dos triángulos, por lo que habrá seis índices por rectángulo.
- 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:
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.
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.
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
gracias por el enlace, no puedo creer que este artículo no apareció en mi Google ... – Mark
sin preocupaciones - contento Yo podría ayudar :-) –