12

The following image illustrates what I am trying to achieve:

Basically I want to create two Path objects that "touch" each other (parallel paths). This is XAML used to generate this image:

<StackPanel Orientation="Horizontal">
    <StackPanel.LayoutTransform>
        <ScaleTransform CenterX="0" CenterY="0" ScaleX="15" ScaleY="15" />
    </StackPanel.LayoutTransform>
    
    <Grid Margin="-5,0,0,0">
        <Path Stroke="Blue">
            <Path.Data>
                <PathGeometry>M10,10 C20,10 10,20 20,20</PathGeometry>
            </Path.Data>
        </Path>
        <Path Stroke="Red">
            <Path.Data>
                <PathGeometry>M10,11 C19,10.85 9,20.80 20,21</PathGeometry>
            </Path.Data>
        </Path>
    </Grid>
    
    <Grid Margin="-5,0,0,0">
        <Path Stroke="Blue">
            <Path.Data>
                <PathGeometry>M10,10 C20,10 10,20 20,20</PathGeometry>
            </Path.Data>
        </Path>
        <Path Stroke="Red">
            <Path.Data>
                <PathGeometry>M10,11 C19,11 9,21 20,21</PathGeometry>
            </Path.Data>
        </Path>
    </Grid>
</StackPanel>

The first curve has hand-optimized point positions, the second has point positions easily calculated by taking stroke thickness into consideration. You can see the second curve is not perfect, because there is a space between the two. How can I create two perfectly "touching" curves programmatically, without hand-optimizing every curve (which is actually not possible because the curves are generated in code)?

Simply put, I generate one curve (resp. Path) in code, and I need it to have two colors. So I thought making second parallel Path would do the trick, but adjusting Geometry of the second Path (to make it parallel) has proven to be problematic.

Update #1

Parallel Lines and Curves by Charles Petzold might be one way to solve this problem. It actually works pretty well, but it flattens the curves, which creates visual artifacts when deeply zoomed, and of course there is a performance drawback.

The algorithm does not, however, attempt to find a Bézier curve that is parallel to another Bézier curve. The algorithm is instead based entirely on polylines: The input is one or more polylines and the output consists of multiple polylines for each input polyline. For this reason, ParallelPath needs to "flatten" the input geometry—which means converting the entire geometry (including arcs and Bézier curves) into a polyline approximation.

Update #2

So a friend of mine (math Ph.D. inceptor) has analyzed this problem and creating parallel curve to the (third-order) Bézier curve is very complex and computationally expensive. For each point of the parallel curve, computer would have to compute something like this:

(degree 3 polynomial) + (degree 2 polynomial) / sqrt(degree 4 polynomial)

Maybe there is a way to optimize this expression, but it would still be MUCH MORE computationally expensive than a standard Bézier curve (because the parallel curve is completely different curve than the original Bézier curve). I want to be able to animate the curve, so this solution would be probably too much CPU expensive. This leaves us with a couple of options:

  1. Use Charles Petzold's polyline approximation, which works wonders, but there are visual glitches when deeply zoomed.

  2. Derive our own approximation based on Charles Petzond's one. Use Bézier curves instead of lines (maybe arcs would be enough). This would solve the deep zoom problem, but it's probably quite hard to code (I have no clue how to do this).

  3. Maybe it is possible to create something like two-color brush. This way, we could use just a single Path to achieve desired result (as shown by the first image). I haven't seen it anywhere though, so this is probably not an option.

Update #3

I've found some pretty interesting links:

More info:

  • QPainterPathStroker from Qt framework is supposed to be using the Thomas F. Hain's algorithm for parallel curves
  • This Java Stroker is also supposed to be capable of drawing parallel curves

Maybe the final solution? (source here)

... I worked out all I knew about Bezier curve theory, and developed the unflattened offsetting to something that is correct, and (monster) documented that on A primer on Bezier curves


Attempt #1

Make second path a bit wider, and slide it underneath the first path while using Z-Index. http://i51.tinypic.com/2r5vwjk.png

This won't work, the Geometry must be transformed accordingly.

Community
  • 1
  • 1
Paya
  • 5,124
  • 4
  • 45
  • 71
  • 1
    Good but hard question, wonder if anyone if going to take up the challenge... – H.B. Apr 07 '11 at 22:02
  • 1
    The thing is that you can't really use the same path for both curves since they are not the same. I wonder if there's a way to create a two-color stroke instead. That way you can use one path but the two colors will be rendered like you want using that single path. – Jeff Mercado Apr 08 '11 at 00:33

6 Answers6

6

Instead of using one fourth-degree Bezier curve, why don't you just use a compund of two quadratic ones? Are you familiar with Bezier curve mathematics? They are favored in graphics because they are quite computationally cheap. I recently created a program where I animated cell movement (just for fun):

enter image description here

The program could easily run in fullscreen on an HD monitor with 100 blobs animated and moving about. And it was all GDI+.

As for parallel Bezier curves, according to Wikipedia it can't really be done: http://en.wikipedia.org/wiki/B%C3%A9zier_curve

So you'll probably have to be happy with an heuristic approach.

EDIT 1:

Lest your curves are completely random, why not create the outline of each curve and then fill the path? The "bottom" curve of one path will be the "top" curve of the other.

EDIT 2:

Ok, as requested, here's how I envision that a "railroadtrack-like" solution can be calculated:

enter image description here

Pedery
  • 3,632
  • 1
  • 27
  • 39
  • In my earlier attempts at this, I tried two quadratic curves and of course was able to get it "perfect." The only problem was that it didn't have the same look to it, it would have to be scaled vertically for a better match. – Jeff Mercado Apr 12 '11 at 02:54
  • Actually I think it was an arc, not even quadratic. – Jeff Mercado Apr 12 '11 at 03:05
  • @Pedery: I'm fine to use a compound of two quadratic curves. The real challenge here is to calculate all 3 points of each curve to make them look perfectly parallel. And this has to be done in code, I can't hand-optimize every curve... This idea is actually something I have already thought of, see `Update #2`/`Option 2`. – Paya Apr 12 '11 at 08:56
  • Why the downvote? I commented on the rendering speed (which *is* fast enough) and that according to wikipedia creating parallel Bezier curves is not possible. **However**, in the quadratic case, each curve is a second degree polynomial. If the heuristic approaches aren't adequate, try calculating the derivative of each curve and offset them along the inverse tangent, like a railroad track. It could get ugly, but could be exactly what you're looking for. – Pedery Apr 12 '11 at 22:33
  • @Jeff: You can get Bezier curves to look pretty similar. Adobe Flash uses Beziers to display circles. All TrueType fonts are rendered with Beziers. It's basically a matter of choosing the correct segments and setting the control points. – Pedery Apr 12 '11 at 22:35
  • @Pedery: I haven't downvoted (I very rarely do in my own questions), so I don't know why. :-) What exactly do you mean by the heuristic approach? I'm not really a math guy, so I need to know a little more. – Paya Apr 13 '11 at 00:41
  • Ah, I see. I also agree with your POW on downvoting since it tends to discourage people from coming with advice. Anyway, heuristics are ways to solve complex problems in an easier way, where you're not as accurate or accept that the solution sometimes only finds local maxima/minima. See the link in my post for hints. Calculating Beziers usually only needs some wit and math for 16-year olds. Again, see my link. – Pedery Apr 13 '11 at 01:37
  • @Pedery: Yeah I've checked the link in and out, and there isn't really anything useful about the heuristics. So what exactly do you suggest as heuristics algorithm? I'm more a programmer than mathematician, so I can't really imagine what should I do to go from "find local maxima/minima" to "draw the final parallel line". Isn't your edit the same as [Timwi's answer](http://stackoverflow.com/questions/5588077/two-color-path-object/5607915#5607915)? Anyway, I've updated my question with some useful links, but I will check them later, don't have time right now. – Paya Apr 13 '11 at 07:52
  • Well, you *should* know how Beziers work if you wanna play with them. They're quite cool actually. Anyway, here goes: For any given line segment between P0 and P1, a point on this segment can be represented as tP0 + (1-t)P1, t:[0,1]. Do the same for points P1 and P2, using the same t. Then do it again between the 1. and the 2. line segments. You'll now have a second degree polynomial that you can derive. Now find the tangent at all points [more...] See also: http://math.stackexchange.com/questions/32225/finding-the-parameter-of-a-quadratic-bezier-curve-for-which-the-tangent-passes-th – Pedery Apr 19 '11 at 02:56
  • From the tangent line in a point, you can calculate the 90-degree turned verson of this line, then get your railroad-track-shifted other lines by moving along this line a fixed number of units. Since you'll be using Beziers, you need to know that the three curves will be different. Quadratic Beziers will do the trick nicely, and it's easy to make them continuous in the endpoints. – Pedery Apr 19 '11 at 03:01
  • @Pedery: If I understand your proposed solution correctly, then it's a polyline approximation, right? Because curve parallel to a bezier curve IS NOT a bezier curve (in the majority of cases). So the only real solution is polyline approximation (which I've already found code for), but it has the already mentioned problem with deep zooming. Or, I found a [multiple-joined-bezier parallel curves](http://processingjs.nihongoresources.com/bezierinfo/) algorithm, which is able to create multiple joined bezier curves to offset a given single bezier curve. – Paya Apr 23 '11 at 23:02
  • @Pedery: Or is your solution different to both of the mentioned solutions? Getting points parallel to a curve isn't a problem, getting tangents, verticals etc is simple. The real problem is to express the parallel curve in a bezier format (see [multiple-joined-bezier parallel curves](http://processingjs.nihongoresources.com/bezierinfo/)), and not in lines (polyline approximation). – Paya Apr 23 '11 at 23:07
  • As long as you can express one of the three curves in terms of a Bezier curve, and the other two as polylines derived from the Bezier curve, even deep zooming shouldn't be an issue. Cause no matter how deep you zoom, you'll still get a smooth curve from the Bezier and then you can railroad-track-derive the other two curves from this. – Pedery Apr 24 '11 at 03:34
  • @Pedery: Could you please attach an image describing what are you talking about? What "three curves"? I've studied the Charles Petzold's polyline approximation and deep zooming is a problem, and always will be, if you approximate curves with this method. The image I included in OP is merely a demonstration of how parallel lines should look like, but I need the solution to be as generic as possible. – Paya Apr 24 '11 at 07:07
  • Edit added. See attached image. – Pedery Apr 25 '11 at 09:42
  • @Pedery: Actually deep zooming is a problem. Charles Petzold's implementation is exactly what are you talking about, and if you deep-zoom the parallel curves, you will see the problem. Maybe if I could [resample the parallel curves depending on the current zoom level](http://stackoverflow.com/questions/5588077/two-color-path-object/5629964#5629964), I could get this working, but this is probably not possible (how is the control going to know if it is being zoomed?). And I can't just use infinite sampling rates, there must be a limit, which is why there is a problem with deep zoom. – Paya Apr 25 '11 at 23:43
  • Yep, I thought it was obvious that you'd need to recalculate the quasiparallel Bezier curves when you zoom. Add an event for it if you wish. Then reaclculate and repaint for each zoom. The control can know if it's being zoomed if you let it. – Pedery Apr 26 '11 at 04:42
4

You said you want to create two Path objects that touch each other, but you didn’t state how the paths are generated. My answer will assume that you have a Path already generated by some algorithm, and you want to turn this into two new paths.

I would switch from trying to use strokes to using fills. If you can automatically create the red path in your second picture, you can equally create a joined path consisting of both and then fill it instead of drawing it with a stroke. Then you do the same thing in two directions.

The result I get for your example looks like this:

Image showing the joined paths

<StackPanel Orientation="Horizontal">
    <StackPanel.LayoutTransform>
        <ScaleTransform CenterX="0" CenterY="0" ScaleX="15" ScaleY="15" />
    </StackPanel.LayoutTransform>

    <Grid Margin="-5,0,0,0">
        <Path Fill="Blue" Stroke="Transparent">
            <Path.Data>
                <PathGeometry>M10,10 C20,10 10,20 20,20 L20,19 C11,19 21,9 10,9</PathGeometry>
                <!--          |←    original path    →| |←  generated part   →| -->
            </Path.Data>
        </Path>
        <Path Fill="Red" Stroke="Transparent">
            <Path.Data>
                <PathGeometry>M10,10 C20,10 10,20 20,20 L20,21 C9,21 19,11 10,11</PathGeometry>
                <!--          |←    original path    →| |←   generated part   →| -->
            </Path.Data>
        </Path>
    </Grid>
</StackPanel>
Timwi
  • 65,159
  • 33
  • 165
  • 230
  • @Timwi: Thanks, +1. This is indeed a nice pragmatic approach. The problem is I want to generate 2 paths with strokes (or fills, doesn't really mater) that touch each other, where their total width is 2 (in every single point), and width of each curve is exactly 1. The curves in your solution have different widths in different points. – Paya Apr 09 '11 at 21:54
  • @Paja: Well, as you already discovered, that is a mathematically very hard problem, so I don’t think you’ll find an easy solution that fulfills that criterion... – Timwi Apr 10 '11 at 19:07
  • @Timwi: I believe I'm not the first to encounter this problem, maybe someone will come up to share his wisdom with us. I'm actually quite surprised WPF doesn't have any support for this. What are all WPF developers doing when they want two-color rectangle with rounded corners? They just use two `Border`s with `CornerRadius`, ignoring the fact that this will look pretty ugly when deeply zoomed? – Paya Apr 10 '11 at 22:04
  • @Paja: I guess they just use fill and stroke. You can draw a rounded rectangle with a blue fill and a thick red outline. – Timwi Apr 11 '11 at 16:40
  • @Paja: I don't think deep zooming is a target scenario for WPF in general (quite rightfuly so in my opinion). High quality deep zooming will have to be handled in application logic not at the rendering layer. – Serguei Apr 12 '11 at 03:13
  • @Serguei: I think this depends on what are you trying to zoom. If bitmaps, then sure, but if you just want to zoom vector graphics (or graphics created purely in XAML), then I don't see a problem here. – Paya Apr 12 '11 at 09:42
1

Consider a slightly different approach to the problem...

Assuming a 'large' number of points on the geometry. It may be feasible to use one of the higher quality interpolaton methods by sampling the geometry points at lower zoom levels. As the zoom increases you can increase the sampling rate and instead render only a subset of the curve; This way the amount of computation should stays relatively constant at all zoom levels. The key is that there is a constant number of pixels on the screen and you can start sampling points once accuracy passes some threshold.

Serguei
  • 2,910
  • 3
  • 24
  • 34
  • Thanks, interesting idea. But what I want is to make the control reusable, and I'm not sure the control can determine the zoom level used by parent control. Imagine the situation in OP, where the `ScaleTransform` is set on `StackPanel`. How is the `MyPath` going to determine it is being zoomed so it needs to resample part of the curve? Pardon me if this is something trivial, but I'm not exactly a WPF expert. – Paya Apr 12 '11 at 08:48
1

Read this... Silverlight - Epic Graphical Fail (rectangle by two triangles) :(

Update

Try this (it's little overkilling but it may help you)

<Grid Margin="-5,0,0,0">
    <Path Stroke="Blue" Data="M10,10 C20,10 10,20 20,20 M20,21 C9,21 19,11 10,11"
            Fill="Blue" Clip="M10,10 C20,10 10,20 20,20 L20,21 C9,21 19,11 10,11"/>
    <Path Stroke="Blue" Data="M10,10 C20,10 10,20 20,20"/>
    <Path Stroke="Red" Data="M10,11 C19,11 9,21 20,21"/>
</Grid>

enter image description here

Community
  • 1
  • 1
obenjiro
  • 3,665
  • 7
  • 44
  • 82
  • Done, but how is this going to help me? They are creating triangles, but I need parallel bezier curves, that's something completely different. – Paya Apr 13 '11 at 13:21
  • Well.. the problem with Silverlight is this.. if you want to draw two paths that you want to be connected with each other (for exaple, parrallel bezier curves), they have to be a part of ONE path object... this is how Silverlight works... – obenjiro Apr 13 '11 at 13:25
  • The real challenge here is to get a parallel bezier curve. Imagine I have only one curve at a start, and I want to get a second parallel one. To make the curve really parallel is quite complex task, I don't even understand the math behind it. – Paya Apr 13 '11 at 18:45
0

Is it possible to make the second path(generated) a bit wider and slide it underneath (behind) the first path using the z-index? You'll get a seamless join that way.

Dave White
  • 3,451
  • 1
  • 20
  • 25
0

Lets say you need two lines of width 5:

If one pixel of difference is not too much you could first draw a curve with let's say width 11 in red, then draw a curve with same path with width 1 in blue, then fill one of the sides with blue.

You would have split the line at half and colored one half, problem is that the splitting in half takes one pixel :(

But what would happen if you chose an even width like 10? Where would the middle pixel be? Maybe you can exploit something...

Marino Šimić
  • 7,318
  • 1
  • 31
  • 61
  • If I understand your suggestion correctly, it would fill the content of one of the `Path`s, right? Meaning you wouldn't get the first image in OP, but something completely different, because one of the `Path`s would be not only stroked, but filled as well (`Path`'s interior wouldn't be transparent as in the first image, but filled with color). Unfortunately, 1 pixel matters. :-( – Paya Apr 13 '11 at 07:35