-
Mar 5th, 2025, 05:02 PM
#1
RC6 Cairo Path "Double" Stroke
Any RC6 Cairo users know if there is a way to perform a second stroke around/outside of a first stroke? Something that looks like this:

I see that it is possible to get the extents of the path including the Stroke using GetStrokeExtents, but using CopyPath doesn't seem to be able to return the Path with the Stroke applied, just the main path points (unless I'm missing something)?
Code:
Option Explicit
Private Sub Form_Click()
Dim x1 As Double, y1 As Double, x2 As Double, y2 As Double
Dim w As Double, h As Double
With Cairo.CreateSurface(Me.ScaleWidth, Me.ScaleHeight)
With .CreateContext
' Fill entire background of surface
.SetSourceColor vbWhite
.Paint
' Draw a simple but arbitrary polygon path
.MoveTo 50, 50
.LineTo 100, 50
.LineTo 300, 300
.LineTo 150, 400
.ClosePath ' Close the polygon
' Get the W/H of the polygon path
.GetPathExtents x1, y1, x2, y2
w = x2 - x1
h = y2 - y1
Debug.Print "UNSTROKED PATH W/H: " & w, h
' Stroke the polygon path
.SetSourceColor vbRed
.SetLineWidth 3
.Stroke True ' Leave the path open to measure it with the stroke applied
' Get the W/H of the path including stroke width
.GetStrokeExtents x1, y1, x2, y2
w = x2 - x1
h = y2 - y1
.ClearPath ' Close the path
Debug.Print "STROKED PATH W/H: " & w, h ' Should be a bit larger than the unstroked path width
' IS THERE ANY WAY TO A CREATE NEW UNSTROKED PATH FROM A STROKED PATH? OR ADD A SECOND STROKE AROUND THE FIRST STROKE?
End With
.DrawToDC Me.hDC
End With
End Sub
-
Mar 5th, 2025, 06:22 PM
#2
Re: RC6 Cairo Path "Double" Stroke
In Olaf's AquaButton example, multiple strokes are achieved by calling DefineButtonPath and CC.Clip/CC.ResetClip multiple times.
https://www.vbforums.com/showthread....=1#post5663051
-
Mar 5th, 2025, 08:51 PM
#3
Re: RC6 Cairo Path "Double" Stroke
A stupid and imprecise approach:
Version1
Code:
Option Explicit
Private Sub Form_Click()
Dim x1 As Double, y1 As Double, x2 As Double, y2 As Double, x3 As Double, y3 As Double, x4 As Double, y4 As Double
Dim w As Double, h As Double, Srf As cCairoSurface, CC As cCairoContext, LineWidth As Double
LineWidth = 3
Set Srf = Cairo.CreateSurface(Me.ScaleWidth, Me.ScaleHeight)
Set CC = Srf.CreateContext
With CC
' Fill entire background of surface
.SetSourceColor vbWhite
.Paint
' Draw a simple but arbitrary polygon path
x1 = 50: y1 = 50
x2 = 100: y2 = 50
x3 = 300: y3 = 300
x4 = 150: y4 = 400
DefinePolygonPath CC, x1, y1, x2, y2, x3, y3, x4, y4 ', vbYellow
CC.Fill True, Cairo.CreateSolidPatternLng(vbYellow)
x1 = x1 - 1: y1 = y1 - 1
x2 = x2 + 1: y2 = y2 - 1
x3 = x3 + 1: y3 = y3 + 1
x4 = x4 - 1: y4 = y4 + 1
DefinePolygonPath CC, x1, y1, x2, y2, x3, y3, x4, y4, vbRed
x1 = x1 - 1: y1 = y1 - 1
x2 = x2 + 1: y2 = y2 - 1
x3 = x3 + 1: y3 = y3 + 1
x4 = x4 - 1: y4 = y4 + 1
DefinePolygonPath CC, x1, y1, x2, y2, x3, y3, x4, y4, vbGreen
End With
Srf.DrawToDC Me.hDC
End Sub
Private Sub DefinePolygonPath(CC As cCairoContext, ByVal x1#, ByVal y1#, ByVal x2#, ByVal y2#, ByVal x3#, ByVal y3#, ByVal x4#, ByVal y4#, _
Optional ByVal StrokeColor& = -1, Optional ByVal Alpha# = 1, Optional ByVal Shade# = 1)
CC.MoveTo x1, y1
CC.LineTo x2, y2
CC.LineTo x3, y3
CC.LineTo x4, y4
CC.ClosePath
If StrokeColor <> -1 Then CC.Stroke , Cairo.CreateSolidPatternLng(StrokeColor, Alpha, Shade)
End Sub
Private Sub Form_Resize()
Me.ScaleMode = vbPixels
End Sub
-
Mar 5th, 2025, 08:53 PM
#4
Re: RC6 Cairo Path "Double" Stroke
Another stupid and imprecise approach:
Version2
Code:
Option Explicit
Private Sub Form_Click()
Dim x1 As Double, y1 As Double, x2 As Double, y2 As Double, x3 As Double, y3 As Double, x4 As Double, y4 As Double
Dim w As Double, h As Double, Srf As cCairoSurface, CC As cCairoContext, LineWidth As Double
Dim LeftOffs#, TopOffs#, RightOffs#, BottomOffs#, x5#, y5#, x6#, y6#
LineWidth = 3
Set Srf = Cairo.CreateSurface(Me.ScaleWidth, Me.ScaleHeight)
Set CC = Srf.CreateContext
With CC
' Fill entire background of surface
.SetSourceColor vbWhite
.Paint
' Draw a simple but arbitrary polygon path
x1 = 50: y1 = 50
x2 = 100: y2 = 50
x3 = 300: y3 = 300
x4 = 150: y4 = 400
DefinePolygonPath CC, x1, y1, x2, y2, x3, y3, x4, y4 ', vbYellow
CC.Fill True, Cairo.CreateSolidPatternLng(vbYellow)
.GetStrokeExtents x5, y5, x6, y6
LeftOffs = 50 - x5
TopOffs = 50 - y5
RightOffs = x6 - 300
BottomOffs = y6 - 400
x1 = x1 - LeftOffs: y1 = y1 - TopOffs
x2 = x2 + RightOffs: y2 = y2 - TopOffs
x3 = x3 + RightOffs: y3 = x3 'y3 + BottomOffs
x4 = x4 - LeftOffs: y4 = y4 + BottomOffs
DefinePolygonPath CC, x1, y1, x2, y2, x3, y3, x4, y4, vbRed
x1 = x1 - LeftOffs: y1 = y1 - TopOffs
x2 = x2 + RightOffs: y2 = y2 - TopOffs
x3 = x3 + RightOffs: y3 = x3 'y3 + BottomOffs
x4 = x4 - LeftOffs: y4 = y4 + BottomOffs
DefinePolygonPath CC, x1, y1, x2, y2, x3, y3, x4, y4, vbGreen
End With
Srf.DrawToDC Me.hDC
End Sub
Private Sub DefinePolygonPath(CC As cCairoContext, ByVal x1#, ByVal y1#, ByVal x2#, ByVal y2#, ByVal x3#, ByVal y3#, ByVal x4#, ByVal y4#, _
Optional ByVal StrokeColor& = -1, Optional ByVal Alpha# = 1, Optional ByVal Shade# = 1)
CC.MoveTo x1, y1
CC.LineTo x2, y2
CC.LineTo x3, y3
CC.LineTo x4, y4
CC.ClosePath
If StrokeColor <> -1 Then CC.Stroke , Cairo.CreateSolidPatternLng(StrokeColor, Alpha, Shade)
End Sub
Private Sub Form_Resize()
Me.ScaleMode = vbPixels
End Sub
-
Mar 5th, 2025, 08:57 PM
#5
Re: RC6 Cairo Path "Double" Stroke
The third stupid and accurate way to do this is to calculate Left-Offsets, Top-Offsets, Right-Offsets, Bottom-Offsets.
When there is no documentation and no help can be obtained through the Object-Browser, we are usually left with a stupid approach.
-
Mar 6th, 2025, 08:52 AM
#6
Re: RC6 Cairo Path "Double" Stroke
Thanks SD - I've also come up with some imprecise methods, but they all look kind of ugly. Thinking about it more, it might be that we need to find the "center" of the path, then loop through each point a change the X/Y values depending on where they are relative to the center:
- If X is left of the center, subtract the stroke width.
- If X is at the center, leave it alone.
- If X is right of center, add the stroke width.
- If Y is above the center, subtract the stroke width.
- If Y is at the center, leave it alone.
- If Y is below the center, add the stroke width.
In my head that seems like it should work, but the proof will be in the rendering. The tricky bit will be finding the center of any arbitrary polygon - maybe it's enough to find the center of the bounding box? I'll give it a try today and post the results.
-
Mar 6th, 2025, 10:00 AM
#7
Re: RC6 Cairo Path "Double" Stroke
If I had to do it in GDI+ I would first fill path with yellow, then clip surface/graphics to path and draw green outline with 8 px wide (centered) pen, then reset clip and draw red outline 2 px wide.
This way half of the 8 px green outline (which gets outsite clip path) is trimmed, so drawing ends up with 4 px green outline only on the inside of path.
cheers,
</wqw>
-
Mar 6th, 2025, 03:41 PM
#8
-
Mar 6th, 2025, 05:29 PM
#9
Re: RC6 Cairo Path "Double" Stroke
Bloody hell - vbforums.com died when I posted my message!!
-
Mar 6th, 2025, 05:34 PM
#10
Re: RC6 Cairo Path "Double" Stroke
Okay, I have something that renders reasonably well, though I suspect there must be a better way as it is quite heavy. Here's the rendered result:

And here's a demo you can play with:
FakeDoubleStroke.zip
To use the demo, click on the form to start a polygon, then move the mouse and click wherever you'd like to anchor a new point. When you are done, press Enter and the polygon will be rendered with a fake double stroke.
-
Mar 7th, 2025, 01:39 AM
#11
Re: RC6 Cairo Path "Double" Stroke
-
Mar 7th, 2025, 07:15 AM
#12
Re: RC6 Cairo Path "Double" Stroke
As long as only "stroking" is concerned - one can achieve something like an
inner-stroke + outer-stroke effect (each with half the choosen line-width) this way:
(using wqwetos clipping-idea)
Code:
Private Sub Form_Load()
ScaleMode = vbPixels
With Cairo.CreateSurface(ScaleWidth, ScaleHeight).CreateContext
' Fill entire background of surface
.Paint 1, Cairo.CreateCheckerPattern
' Define a simple but arbitrary polygon path
.MoveTo 50, 50
.LineTo 100, 50
.LineTo 300, 300
.LineTo 150, 400
.ClosePath ' close the polygon-path (in the sense, of connecting the last point to the first one)
.SetLineWidth 10 'set a somewhat thicker linewidth, to make the "half-width-lines" easier to recognize
.Stroke True, Cairo.CreateSolidPatternLng(vbGreen) 'normal (full LineWidth-)Stroke
.Clip True 'now set a clip on the poly-path (so that only inside this area, drawings can manifest)
.Stroke True, Cairo.CreateSolidPatternLng(vbRed) 'additional (half-)stroke - only on the inside
.ResetClip
.ClearPath
Set Picture = .Surface.Picture
End With
End Sub
HTH
Olaf
Last edited by Schmidt; Mar 7th, 2025 at 08:12 AM.
-
Mar 7th, 2025, 09:18 AM
#13
Re: RC6 Cairo Path "Double" Stroke
 Originally Posted by Schmidt
As long as only "stroking" is concerned - one can achieve something like an
inner-stroke + outer-stroke effect (each with half the choosen line-width) this way:
(using wqwetos clipping-idea)
Thanks Olaf, that's definitely much cleaner than my attempt, with the downside that the stroke is an inner stroke. Ideally, I'd like to do an outer stroke because my user's will be using drawing tools to call out certain areas of photographs, and they might be drawing fairly tight polygons around areas of interest. Something like this:

The goal is to have a second stroke outside for when a polygon has been selected (to move it, delete it, etc...) and I'd prefer not to obscure any of the inner portion/area of interest.
I'm probably being persnickety, but I've tried just stroking with a highlight colour over top the existing stroke when an object is selected. This wasn't too bad, but then I need to do some tests to make sure that the selection stroke is definitely a different colour/style than the unselected polygon stroke to ensure maxium visibility, and this has the downside of the selection strokes potentially looking different for different kinds of selected objects in multi-select scenarios. Another option would be to just stroke the bounding box, but it's not as "cool" looking.
I was hoping that since Cairo knows the size and shape of the stroked path, that there might be someway to get it to spit out an expanded path ready to re-stroke, but it appears not. I've been doing some more experiments on algos for expanding a path and I'm getting closer by finding the left-most point, and looping around points to determine if there are changes in line direction and adjusting the X/Y coords by an offset, but it's not quite good enough as I'm still miscalculating which way/how far to offset in some cases.
Another option I just found would be to use a third-party library...the Clipper2 library looks interesting...
-
Mar 7th, 2025, 01:12 PM
#14
Re: RC6 Cairo Path "Double" Stroke
I have these two ideas:
The simple one is maybe similar to Olaf's. But I doubt it's enough.
The advanced one applies the offset (positive or negative / external or internal) to the polygon by recalculating the coordinates of the points.
It basically moves the segments according to their normals and then calculates the points of the new polygon by finding the intersections between them.
Code:
Option Explicit
Private Sub Form_Click()
Dim Srf As cCairoSurface, CC As cCairoContext
Set Srf = Cairo.CreateSurface(Me.ScaleWidth, Me.ScaleHeight)
Set CC = Srf.CreateContext
Dim PolyCoord() As Single
Dim PolyCoordOUT() As Single
Dim N As Long
N = 4 * 2 - 1
ReDim PolyCoord(N)
PolyCoord(0) = 50: PolyCoord(1) = 50
PolyCoord(2) = 100: PolyCoord(3) = 50
PolyCoord(4) = 300: PolyCoord(5) = 300
PolyCoord(6) = 150: PolyCoord(7) = 400
'SIMPLER MODE
CC.SetSourceColor vbWhite: CC.Paint
CC.SetSourceColor vbRed: CC.SetLineWidth 12
CC.PolygonSingle PolyCoord(), True
CC.Stroke
CC.SetSourceColor vbYellow
CC.PolygonSingle PolyCoord(), True
CC.Fill
CC.SetSourceColor vbGreen: CC.SetLineWidth 6
CC.PolygonSingle PolyCoord(), True
CC.Stroke
'--------------------------------------------------
'Advanced mode (using line-line intersection)
Dim I As Long
For I = 0 To N Step 2 ' offset right X coords
PolyCoord(I) = PolyCoord(I) + 300
Next
CC.SetSourceColor vbYellow
CC.PolygonSingle PolyCoord(), True
CC.Fill
CC.SetSourceColor vbGreen: CC.SetLineWidth 4
CC.PolygonSingle PolyCoord(), True
CC.Stroke
OFFSETPOLYGON PolyCoord, PolyCoordOUT, 4
CC.SetSourceColor vbRed: CC.SetLineWidth 4
CC.PolygonSingle PolyCoordOUT(), True
CC.Stroke
Srf.DrawToDC Me.hDC
End Sub
Private Sub OFFSETPOLYGON(PolyIn() As Single, PolyOut() As Single, Offset As Single)
Dim U As Long
Dim X1 As Long, Y1 As Long
Dim X2 As Long, Y2 As Long
Dim X3 As Long, Y3 As Long
Dim X4 As Long, Y4 As Long
Dim U1 As Long
Dim PolySideNormal() As Single
U = UBound(PolyIn)
ReDim PolySideNormal(U)
ReDim PolyOut(U)
Dim Dx!, Dy!
'Compute Segments Normals
U1 = U + 1
For X1 = 0 To U Step 2
Y1 = X1 + 1
X2 = (X1 + 2) Mod (U1)
Y2 = (Y1 + 2) Mod (U1)
Dx = PolyIn(X2) - PolyIn(X1)
Dy = PolyIn(Y2) - PolyIn(Y1)
Normalize Dx, Dy
PolySideNormal(X1) = Dy
PolySideNormal(Y1) = -Dx
Next
' OFFSET SEGMENTS and FIND INTERSECTION
Dim xx1!, yy1, xx2!, yy2!, xx3!, yy3!, xx4!, yy4!
Dim RX!, RY!
For X1 = 0 To U Step 2
Y1 = X1 + 1
X2 = (X1 + 2) Mod (U1)
Y2 = (Y1 + 2) Mod (U1)
X3 = X1 - 2: If X3 < 0 Then X3 = U1 + X3
Y3 = Y1 - 2: If Y3 < 0 Then Y3 = U1 + Y3
xx1 = PolyIn(X3) + PolySideNormal(X3) * Offset
yy1 = PolyIn(Y3) + PolySideNormal(Y3) * Offset
xx2 = PolyIn(X1) + PolySideNormal(X3) * Offset
yy2 = PolyIn(Y1) + PolySideNormal(Y3) * Offset
xx3 = PolyIn(X1) + PolySideNormal(X1) * Offset
yy3 = PolyIn(Y1) + PolySideNormal(Y1) * Offset
xx4 = PolyIn(X2) + PolySideNormal(X1) * Offset
yy4 = PolyIn(Y2) + PolySideNormal(Y1) * Offset
LINELINE xx1, yy1, xx2, yy2, xx3, yy3, xx4, yy4, RX, RY
PolyOut(X1) = RX
PolyOut(Y1) = RY
Next
End Sub
Private Sub Form_Resize()
Me.ScaleMode = vbPixels
End Sub
Private Sub Normalize(ByRef Dx!, ByRef Dy!)
Dim D!
D = 1! / Sqr(Dx * Dx + Dy * Dy)
Dx = Dx * D
Dy = Dy * D
End Sub
Private Sub LINELINE(ByVal Ax!, ByVal Ay!, ByVal Bx!, ByVal By!, _
ByVal Cx!, ByVal Cy!, ByVal Dx!, ByVal Dy!, _
ByRef RX!, ByRef RY!)
Dim Nu!, De!, S!
Dim BAx!, BAy!
Dim DCx!, DCy!
BAx = Bx - Ax
BAy = By - Ay
DCx = Dx - Cx
DCy = Dy - Cy
Nu! = ((BAx * (Ay - Cy)) + ((BAy) * Cx) - ((BAy) * Ax))
De! = ((BAx) * (DCy)) - ((BAy) * (DCx))
S = Nu / De
RX = Cx + (Dx - Cx) * S
RY = Cy + (Dy - Cy) * S
End Sub
Last edited by reexre; Mar 7th, 2025 at 02:51 PM.
-
Mar 7th, 2025, 01:17 PM
#15
Fanatic Member
Re: RC6 Cairo Path "Double" Stroke
GDI+ has a function that converts a path to it's outline points only (GdipWindingModeOutline)
https://learn.microsoft.com/en-us/wi...cspath-outline
The above has a code example that draws an outline with a 1px wide pen around a path made with a 10px wide pen.
One line function call to construct the outline path.
Perhaps RC6 Cairo has a similar function??
Last edited by mms_; Mar 7th, 2025 at 01:25 PM.
-
Mar 7th, 2025, 02:55 PM
#16
Re: RC6 Cairo Path "Double" Stroke
 Originally Posted by reexre
I have these two ideas:
The simple one is maybe similar to Olaf's. But I doubt it's enough.
The advanced one applies the offset (positive or negative / external or internal) to the polygon by recalculating the coordinates of the points.
It basically moves the segments according to their normals and then calculates the points of the new polygon by finding the intersections between them.
The advanced method is *perfect*! I've been trying to do something similar with the idea of pushing the segments out and extending them, but clearly my math wasn't up to snuff, so thank you!
Here's an example of the output:

One question - is there a reason you chose to work with Singles instead of Doubles? Just curious because Cairo does a lot of its work at double precision.
Also - in case anyone passing by uses reexre's code in the future: I had to adjust my point dropping/locking code to avoid double entries in case of user double-click, otherwise there would be a division by zero error in the Normalize sub. If this happens to you, then your points are too close together.
-
Mar 7th, 2025, 03:08 PM
#17
Re: RC6 Cairo Path "Double" Stroke
Thank you jpbro
 Originally Posted by jpbro
One question - is there a reason you chose to work with Singles instead of Doubles? Just curious because Cairo does a lot of its work at double precision.
ah, no, there is no reason, I just temporarily forgot about CC.Polygon (I don't know why but I remembered there was only CC.PolygonSingle)
Replace everywhere Single with Double and ! with #
PS
about line line intersection I took the formulas from this video https://www.youtube.com/watch?v=CLX3AwSc3Zc
Last edited by reexre; Mar 7th, 2025 at 03:13 PM.
-
Mar 7th, 2025, 03:23 PM
#18
Re: RC6 Cairo Path "Double" Stroke
 Originally Posted by reexre
ah, no, there is no reason, I just temporarily forgot about CC.Polygon (I don't know why but I remembered there was only CC.PolygonSingle)
Replace everywhere Single with Double and ! with #
Got it! Just making sure there wasn't a good reason for Single that I didn't know about 
 Originally Posted by reexre
Thanks for the video, I'll check it out.
To show how well your code works, you can stack strokes for quite a while and it looks great:

Great job, thank you!
-
Mar 7th, 2025, 03:37 PM
#19
Re: RC6 Cairo Path "Double" Stroke
maybe the code could be improved a bit, for example avoiding redundant operations, but it was written in a hurry
EG
Code:
PolySideNormal(X1) = Dy
PolySideNormal(Y1) = -Dx
could be
Code:
PolySideNormal(X1) = Dy * Offset
PolySideNormal(Y1) = -Dx * Offset
and all ...
Code:
xx1 = PolyIn(X3) + PolySideNormal(X3) * Offset
to
Code:
xx1 = PolyIn(X3) + PolySideNormal(X3)
.... And something else similar
-
Mar 7th, 2025, 04:35 PM
#20
Re: RC6 Cairo Path "Double" Stroke
 Originally Posted by reexre
maybe the code could be improved a bit, for example avoiding redundant operations, but it was written in a hurry
EG
Thanks for the tweaks, but it's plenty good enough for my use case as it is. Right it handles ~200 points in about 0.1ms I wouldn't expect users to be getting close to that many points very often.
I'm just tweaking it a bit now to get it to take a single RC6.cArrayList of cPoint objects to perform an inplace value swap/expansion, and that way I can skip a step of converting my points to an array of Doubles. I'm using the ArrayList because it makes it easier to insert/remove points at arbitrary positions should the user choose to do so.
Last edited by jpbro; Mar 7th, 2025 at 07:15 PM.
-
Mar 8th, 2025, 03:19 PM
#21
Re: RC6 Cairo Path "Double" Stroke
I spent some time trying to understand reexre's code and modified it a bit to work with a cArrayList of CPoint classes instead of an array of Doubles. I also changed it so that it would work properly with polygons that were drawn counter-clockwise (the original method would perform an inner stroke in this case), and implemented reexre's suggested improvement.
Finally, I changed a bunch of variable names along the way while I was improving my understanding of the algorithms involved, and asked ChatGPT to add some comments that were reasonably helpful.
The final code is fast and does exactly what I needed it to do. Thanks to everyone for your input, and especially reexre for the code contribution, much appreciated!
SOURCE: MultiStrokePolygonPath.zip
Check out the timings on base polygon of 180 points, and a 6x expansion/stroke:

Not too shabby!
-
Mar 16th, 2025, 03:33 PM
#22
Re: RC6 Cairo Path "Double" Stroke
Just completed a demo project that makes use of double-stroking polygons and and whole bunch of other Cairo drawing stuff to allow users to mark-up/annotate images here:
https://www.vbforums.com/showthread....e-Markup-Tools
Thanks again to everyone for their help, and especially reexre for the near-perfect polygon expansion code. Much appreciated!
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|