Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>What you're looking for is effectively a non-linear transform. The Transform property on Visual can only do linear transforms. Fortunately WPF's 3D features come to your rescue. You can easily accomplish what you are looking for by creating a simple custom control that would be used like this:</p> <pre><code>&lt;local:DisplayOnPath Path="{Binding ...}" Content="Text to display" /&gt; </code></pre> <p>Here is how to do it:</p> <p>First create the "DisplayOnPath" custom control.</p> <ol> <li>Create it using Visual Studio's custom control template (making sure your assembly:ThemeInfo attribute is set correctly and all that)</li> <li>Add a dependency property "Path" of type <code>Geometry</code> (use wpfdp snippet)</li> <li>Add a read-only dependency property "DisplayMesh" of type <code>Geometry3D</code> (use wpfdpro snippet)</li> <li>Add a <code>PropertyChangedCallback</code> for Path to call a "ComputeDisplayMesh" method to convert the Path to a Geometry3D, then set DisplayMesh from it</li> </ol> <p>It will look something like this:</p> <pre><code>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) =&gt; { var displayOnPath = obj as DisplayOnPath; displayOnPath.DisplayMesh = ComputeDisplayMesh(displayOnPath.Path); })); public Geometry3D DisplayMesh { get { ... } private set { ... } } private static DependencyPropertyKey DisplayMeshPropertyKey = ... public static DependencyProperty DisplayMeshProperty = ... } </code></pre> <p>Next create the style and control template in <code>Themes/Generic.xaml</code> (or a <code>ResourceDictionary</code> included by it) as for any custom control. The template will have contents like this:</p> <pre><code>&lt;Style TargetType="{x:Type local:DisplayOnPath}"&gt; &lt;Setter Property="Template"&gt; &lt;Setter.Value&gt; &lt;ControlTemplate TargetType="{x:Type local:DisplayOnPath}"&gt; &lt;Viewport3DVisual ...&gt; &lt;ModelVisual3D&gt; &lt;ModelVisual3D.Content&gt; &lt;GeometryModel3D Geometry="{Binding DisplayMesh, RelativeSource={RelativeSource TemplatedParent}}"&gt; &lt;GeometryModel3D.Material&gt; &lt;DiffuseMaterial&gt; &lt;DiffuseMaterial.Brush&gt; &lt;VisualBrush ...&gt; &lt;VisualBrush.Visual&gt; &lt;ContentPresenter /&gt; ... </code></pre> <p>What this does is display a 3D model that uses a DisplayMesh for location and uses your control's Content as a brush material.</p> <p>Note that you may need to set other properties on the Viewport3DVisual and VisualBrush to get the layout to work the way you want and for the content visual to be stretched appropriately.</p> <p>All that is left is the "ComputeDisplayMesh" function. This is a trivial mapping if you want the top of the content (the words you are displaying) to be perpendicular a certain distance out from the path. Of course, there are other algorithms you might choose instead, such as to create a parallel path and use percent distance along each.</p> <p>In any case, the basic algorithm is the same:</p> <ol> <li>Convert to <code>PathGeometry</code> using <code>PathGeometry.CreateFromGeometry</code></li> <li>Select an appropriate number of rectangles in your mesh, 'n', using a heuristic of your choice. Maybe start with hard-coding n=50.</li> <li>Compute your <code>Positions</code> values for all the corners of the rectangles. There are n+1 corners on top and n+1 corners on the bottom. Each bottom corner can be found by calling <code>PathGeometry.GetPointAtFractionOfLength</code>. This also returns a tangent, so it is easy to find the top corner as well.</li> <li>Compute your <code>TriangleIndices</code>. This is trivial. Each rectangle will be two triangles, so there will be six indices per rectangle.</li> <li>Compute your <code>TextureCoordinates</code>. This is even more trivial, because they will all be 0, 1, or i/n (where i is the rectangle index).</li> </ol> <p>Note that if you are using a fixed value of n, the only thing you ever have to recompute when the path changes is the <code>Posisions</code> array. Everything else is fixed.</p> <p>Here is the what the main part of this method looks like:</p> <pre><code>var pathGeometry = PathGeometry.CreateFromGeometry(path); int n=50; // Compute points in 2D var positions = new List&lt;Point&gt;(); for(int i=0; i&lt;=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&lt;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 </code></pre> <p>That's about it. Most of the code is written above. I leave it to you to fill in the rest.</p> <p>By the way, note that by being creative with the 'Z' value, you can get some truly awesome effects.</p> <p><strong>Update</strong></p> <p>Mark implemented the code for this and encountered three problems. Here are the problems and the solutions for them:</p> <ol> <li><p>I made a mistake in my TriangleIndices order for triangle #1. It is corrected above. I originally had those indices going upper left - lower left - upper right. By going around the triangle counterclockwise we actually saw the back of the triangle so nothing was painted. By simply changing the order of the indices we go around clockwise so the triangle is visible.</p></li> <li><p>The binding on the GeometryModel3D was originally a <code>TemplateBinding</code>. This didn't work because TemplateBinding doesn't handle updates the same way. Changing it to a regular binding fixed the problem.</p></li> <li><p>The coordinate system for 3D is +Y is up, whereas for 2D +Y is down, so the path appeared upside-down. This can be solved by either negating Y in the code or by adding a <code>RenderTransform</code> on the <code>ViewPort3DVisual</code>, as you prefer. I personally prefer the RenderTransform because it makes the ComputeDisplayMesh code more readable.</p></li> </ol> <p>Here is a snapshot of Mark's code animating a sentiment I think we all share:</p> <p><a href="http://rayburnsresume.com/StackOverflowImages/WavyLetters.png">Snapshot of animating text &quot;StackOverflowIsFun&quot; http://rayburnsresume.com/StackOverflowImages/WavyLetters.png</a></p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload