Create a path out of arcs and lines that can be filled

LiveCode Builder is a language for extending LiveCode's capabilities, creating new object types as Widgets, and libraries that access lower-level APIs in OSes, applications, and DLLs.

Moderators: LCMark, LCfraser

Post Reply
trevordevore
VIP Livecode Opensource Backer
VIP Livecode Opensource Backer
Posts: 1005
Joined: Sat Apr 08, 2006 3:06 pm
Location: Overland Park, Kansas
Contact:

Create a path out of arcs and lines that can be filled

Post by trevordevore » Sun Mar 29, 2015 4:05 am

I'm trying to make a shape for a widget that will be used with 'popup widget'. The shape is essentially a rounded rect with a triangle in the top right corner. I need to stroke this path and fill this path.

If I was just going to fill the path then creating a round rect path, a triangle path, and then joining them using 'add tPath2 to tPath' would do the trick. The problem is that if you stroke this path then the entire round rect is stroked. I want the stroke to follow the the outer edge, including the triangle.

I've tried creating the shape by hand using 'line to' and 'put arc path centered at ...'. I am able to create the shape and it looks right when I stroke it. If I try to fill it, however, then you can tell that the path I created isn't one complete path.

While I could combine the two techniques and end up with what I need, I would be interested in knowing how to create one path that can be stroked and filled. I've attached screenshots of what I've come up with so far. The first two examples are from the code below. The left is being stroked and filled. The middle is being stroked. The right one is from an attempt to join rounded rect and triangle paths. Any idea as to how I can make this one path that can be filled and stroked?

Code: Select all

widget com.livecode.extensions.screensteps.themedMenu

	use com.livecode.canvas
	use com.livecode.widget
	use com.livecode.engine

	metadata title is "???"
	metadata author is "???"
	metadata version is "1.0.0"
	
	private variable mMenuWidth       as Number
	private variable mMenu            as List
	private variable mMenuItemHeight  as Number
	private variable mBackgroundColor as Color
	private variable mTopBottomMargin as Number
	private variable mChevronBounds   as Rectangle
	private variable mAnchorSide      as String
	private variable mChevronXOffset  as Number
	private variable mCornerRadius    as Number
	private variable mPath            as Path
	
	public handler OnCreate()
		put 177 into mMenuWidth
		put ["My Article", "My Other Article", "My Other Other Article", "-", "New Article"] into mMenu
		put 31 into mMenuItemHeight
		put 10 into mTopBottomMargin
		put rectangle [0,0,15,9] into mChevronBounds
		put color [42/255, 42/255, 42/255] into mBackgroundColor
		put "right" into mAnchorSide
		put 10 into mChevronXOffset
		put 8 into mCornerRadius
		put path "" into mPath
	end handler
	
	public handler OnOpen()
		calculatePositions()
	end handler
	
	public handler OnPaint()
		set the paint of this canvas to solid paint \
			with color [the red of mBackgroundColor, the green of mBackgroundColor, the blue of mBackgroundColor, 0.9]
		fill mPath on this canvas
		
		set the paint of this canvas to solid paint with mBackgroundColor
		stroke mPath on this canvas
	end handler
	
	public handler OnClick()
		if currently popped up then
			close popup -- returning SOMETHING
		end if
	end handler
	
	
	private handler calculatePositions()
		variable tElement
		variable tHeight as Number
		
		put the height of mChevronBounds into tHeight
		add mTopBottomMargin * 2 to tHeight
		
		repeat for each element tElement in mMenu
			if tElement is "-" then
				add 1 to tHeight
			else
				add mMenuItemHeight to tHeight
			end if
		end repeat
		
		variable tPath as Path
		variable tPoint as Point
		
		put path "" into tPath
		put path "" into mPath
		
		if mAnchorSide is "right" then
			-- Top-left
			put point [mCornerRadius/2, the height of mChevronBounds+mCornerRadius/2] into tPoint
			
			put arc path centered at tPoint with radius mCornerRadius from 180 to 270 into tPath
			add tPath to mPath
			
			-- Top
			set the x of tPoint to mMenuWidth-mChevronXOffset-the width of mChevronBounds
			set the y of tPoint to the height of mChevronBounds
			line to tPoint on mPath
		
			-- Chevron
			add the width of mChevronBounds/2 to the x of tPoint
			put 0 into the y of tPoint
			line to tPoint on mPath
			
			add the width of mChevronBounds/2 to the x of tPoint
			put the height of mChevronBounds into the y of tPoint
			line to tPoint on mPath
			
			-- Chevron offset
			variable tDistance as Number
			
			if mChevronXOffset > mCornerRadius then
				-- the arc will cover just the radius
				set the x of tPoint to mMenuWidth - mCornerRadius/2
				line to tPoint on mPath
			end if
			
			-- Top-right corner
			set the x of tPoint to mMenuWidth - mCornerRadius/2
			set the y of tPoint to the height of mChevronBounds + mCornerRadius/2
			put arc path centered at tPoint with radius mCornerRadius from 270 to 360 into tPath
			add tPath to mPath
			
			-- Right side
			put point [mMenuWidth, tHeight - mCornerRadius/2] into tPoint
			line to tPoint on mPath
			
			-- Bottom-right corner
			set the x of tPoint to mMenuWidth - mCornerRadius/2
			put arc path centered at tPoint with radius mCornerRadius from 0 to 90 into tPath
			add tPath to mPath
			
			-- Bottom
			put point [mCornerRadius/2, tHeight] into tPoint
			line to tPoint on mPath
			
			-- Bottom-left
			put point [mCornerRadius/2, tHeight- mCornerRadius/2] into tPoint
			put arc path centered at tPoint with radius mCornerRadius from 90 to 180 into tPath
			add tPath to mPath
			
			-- Left
			set the x of tPoint to 0
			move to tPoint on mPath
			
			set the y of tPoint to the height of mChevronBounds+mCornerRadius/2 
			line to tPoint on mPath
		else
		
		end if
		
	end handler
	
end widget
Attachments
examples.png
Examples of what I currently have
examples.png (5.19 KiB) Viewed 4682 times
Trevor DeVore
ScreenSteps - https://www.screensteps.com

LiveCode Repos - https://github.com/search?q=user%3Atrevordevore+topic:livecode
LiveCode Builder Repos - https://github.com/search?q=user%3Atrevordevore+topic:livecode-builder

Zryip TheSlug
VIP Livecode Opensource Backer
VIP Livecode Opensource Backer
Posts: 163
Joined: Tue Jan 26, 2010 10:15 pm
Contact:

Re: Create a path out of arcs and lines that can be filled

Post by Zryip TheSlug » Sun Mar 29, 2015 7:07 pm

@trevordevore

setting the fill rule to "non zero" should have do the trick but in your case I don't understand why applying this rule has no effect.
According to the SVG documentation the rule is applied for shapes drawn from left towards right.

By the way there is a mistake in the documentation. The rule is "non zero" not "non-zero"

In the following example the shape is filled correctly:

Code: Select all

public handler OnPaint()
  variable tPath as Path

  put path "M50,20 l40,40 l-40,40 l-40,-40 l40,-40 M50,40 l20,20 l-20,20 l-20,-20 l20,-20" into tPath

  set the paint of this canvas to solid paint with color [245/255, 10/255,10/255]
  stroke tPath on this canvas
  set the fill rule of this canvas to "non zero"
  set the paint of this canvas to solid paint with color [37/255,90/255,170/255]
  fill tPath on this canvas
end handler
However if we are commenting the line

Code: Select all

set the fill rule of this canvas to "non zero"
we are in a pretty similar behavior than yours.

Maybe a clue.
TheSlug
http://www.aslugontheroad.com - Tutorials, demo stacks and plugins for LiveCode
Data Grid Helper - An intuitive interface for building LiveCode's Data Grids
Excel Library- Extends the LiveCode language for controlling MS Excel

LCMark
Livecode Staff Member
Livecode Staff Member
Posts: 1212
Joined: Thu Apr 11, 2013 11:27 am

Re: Create a path out of arcs and lines that can be filled

Post by LCMark » Mon Mar 30, 2015 8:46 am

The fill-rule only affects filling, not stroking. The non-zero rule uses the direction of the subpaths to determine what is filled - if all your paths go the same way, the union of the areas they cover will be filled; if they go in different directions then whether an area is filled depends on the sum of the directions of the paths passed through to reach it when looking from left to right (take -1 for anti-clockwise, 1 for clockwise, an area is filled if the sum is non-zero). The even-odd rule just looks at the number of paths crossed to reach a particular point - if it is even then the area is not filled, otherwise it is.

If you want to stroke an outline, then your path (paths) must be just that outline with no intersections or joins. i.e. You need to construct a path which starts at one point on the outline and traces all the way round. You can do this using the move to / line to etc. commands. @runrevian finished off two variants of 'arc to' (arc to and arc through) the other week, this has still to be merged in but it is here: https://github.com/runrev/livecode/pull/2103.

Incidentally, what you are seeing when stroking and filling the path, is the 'fill' part is automatically closing the individual subpaths you are creating. This will create an implicit line between the start and end point of the subpath (so that there is an 'interior' to fill), this isn't done for stroking which produces an area to fill by 'thickening' just the path segments you have specified.

trevordevore
VIP Livecode Opensource Backer
VIP Livecode Opensource Backer
Posts: 1005
Joined: Sat Apr 08, 2006 3:06 pm
Location: Overland Park, Kansas
Contact:

Re: Create a path out of arcs and lines that can be filled

Post by trevordevore » Mon Mar 30, 2015 2:28 pm

@LCMark - using 'arc through point' from @runrevian's pull request worked a treat. Much less code and everything fills and strokes as it should. Final code is below.

Code: Select all

widget com.livecode.extensions.test.themedMenu

	use com.livecode.canvas
	use com.livecode.widget
	use com.livecode.engine

	metadata title is "???"
	metadata author is "Trevor DeVore"
	metadata version is "1.0.0"
	
	private variable mMenuWidth       as Number
	private variable mMenu            as List
	private variable mMenuItemHeight  as Number
	private variable mBackgroundColor as Color
	private variable mBorderColor     as Color
	private variable mTopBottomMargin as Number
	private variable mChevronBounds   as Rectangle
	private variable mAnchorSide      as String
	private variable mChevronXOffset  as Number
	private variable mCornerRadius    as Number
	private variable mPath            as Path
	
	public handler OnCreate()
		put 177 into mMenuWidth
		put ["My Article", "My Other Article", "My Other Other Article", "-", "New Article"] into mMenu
		put 31 into mMenuItemHeight
		put 10 into mTopBottomMargin
		put rectangle [0,0,15,9] into mChevronBounds
		put color [42/255, 42/255, 42/255,0.9] into mBackgroundColor
		put color [11/255,11/255,10/255,1] into mBorderColor
		put "right" into mAnchorSide
		put 10 into mChevronXOffset
		put 8 into mCornerRadius
		put path "" into mPath
	end handler
	
	public handler OnOpen()
		calculatePositions()
	end handler
	
	public handler OnPaint()	
		set the stroke width of this canvas to 1
		set the paint of this canvas to solid paint with mBackgroundColor
		fill mPath on this canvas
	
		set the paint of this canvas to solid paint with mBorderColor
		stroke mPath on this canvas
	end handler
	
	public handler OnClick()
		if currently popped up then
			close popup -- returning SOMETHING
		end if
	end handler
	
	
	private handler calculatePositions()
		variable tElement
		variable tHeight as Number
		
		put the height of mChevronBounds into tHeight
		add mTopBottomMargin * 2 to tHeight
		
		repeat for each element tElement in mMenu
			if tElement is "-" then
				add 1 to tHeight
			else
				add mMenuItemHeight to tHeight
			end if
		end repeat
		
		variable tPoint as Point
		variable tStrokeOffset as Number
		
		put path "" into mPath
		
		-- stroke is 1 so offset edges by 0.5 so that stroke stays within my bounds
		put 0.5 into tStrokeOffset
		
		if mAnchorSide is "right" then
		
			move to point [tStrokeOffset,the height of mChevronBounds+mCornerRadius] on mPath
			
			-- Top-left		
			arc through point [tStrokeOffset,the height of mChevronBounds] \
					to point [mMenuWidth-mChevronXOffset-the width of mChevronBounds, the height of mChevronBounds] with radius mCornerRadius on mPath
		
			-- Chevron
			put point [mMenuWidth-mChevronXOffset-the width of mChevronBounds/2, tStrokeOffset] into tPoint
			line to tPoint on mPath
			
			add the width of mChevronBounds/2 to the x of tPoint
			put the height of mChevronBounds into the y of tPoint
			line to tPoint on mPath
			
			-- Top-Right
			arc through point [mMenuWidth-tStrokeOffset,the height of mChevronBounds] \
					to point [mMenuWidth-tStrokeOffset, tHeight - mCornerRadius] with radius mCornerRadius on mPath
			
			-- Bottom-Right
			arc through point [mMenuWidth-tStrokeOffset,tHeight-tStrokeOffset] \
					to point [mCornerRadius, tHeight-tStrokeOffset] with radius mCornerRadius on mPath
					
			-- Bottom-Left
			arc through point [tStrokeOffset,tHeight-tStrokeOffset] \
					to point [tStrokeOffset, tHeight - mCornerRadius] with radius mCornerRadius on mPath
			
			-- Left
			close path on mPath
		else
		
		end if
		
	end handler
	
end widget
Attachments
successfull_fill.png
successfull_fill.png (2.18 KiB) Viewed 4588 times
Trevor DeVore
ScreenSteps - https://www.screensteps.com

LiveCode Repos - https://github.com/search?q=user%3Atrevordevore+topic:livecode
LiveCode Builder Repos - https://github.com/search?q=user%3Atrevordevore+topic:livecode-builder

Post Reply

Return to “LiveCode Builder”