Paintable Canvas Hacks

You can use the technique I outlined here to filter out problematic instances:

In this case, the code would simply be:

	if index != cherryCount - 1:
		paintCherry(graphics, x - cherryRadius, y - cherryRadius, cherryDiameter)

Result:

I'll also add that keeping the points in a clockwise order makes it possible to use them directly in creating Polygons.

That's a good point. In case you don't mind losing out on Polygon ability, here's a generator modified to return (mostly) y-sorted points by "zig-zagging" around the circle. Using a large enough degree offset will break this but it works decently otherwise.

def getYSortedSpacedOvalPoints(points, centerX, centerY, width, height, degreeOffset = 0):
    #same as getSpacedOvalPoints but returns points from top to bottom.
    #degreeOffset technically breaks the ordering but for small offsets should not be noticeable
    radiusWidth = width / 2
    radiusHeight = height / 2
    
    # java.lang.Math trigonometric functions requires radians
    startAngle = Math.toRadians(degreeOffset) # ...or (3.14 * degreeOffset) / 180 to eliminate the import
    
    # Set the end angle to 360 degrees more than the starting angle
    endAngle = startAngle + (2 * Math.PI) # This could also be written startAngle + Math.toRadians(360)
    
    # Abort the operation if there aren't at least two points to avoid a potential division error and other wierdness
    if points <= 1:
        yield centerX, centerY
        return
    
    # Iterate through each point, and yield a coordinate set for the current angle and distance from origin
    angleIncrement = (endAngle - startAngle) / float(points)
    for point in xrange(points):
        angle = startAngle + (((point+1)//2) * angleIncrement) * (-1 if (point % 2 == 1) else 1)
        yield ( # Polar-to-Cartesian conversions
            int(centerX + Math.sin(angle) * radiusWidth),
            int(centerY - Math.cos(angle) * radiusHeight))

Using this should reduce the number of problem points that need to be skipped without having to store a sorted list in memory.

EDIT: Noticed the spacing now looks a little off on the frontmost dabs :frowning: Spacing calculation probably needs adjustment

EDIT 2: I found the issue(s) after thinking for 20 minutes I had some weird floating point error accumulation. First, angle increment now divides by points instead of points-1.
2nd, the dab count was using float division which was giving a non-integer number of dabs, which threw off the increment calculation and maybe was the reason points-1 was needed as an adjustment earlier.

#dabCount = estimatedCircumference / dabDiameter
#changed to
dabCount = estimatedCircumference // dabDiameter

Before:

After:

Functions for Wrapping Text Around a Cylinder or Curving Text Around an Ellipse

Before we can perform either of these functions, we will need to be able to calculate the circumference of the ellipse, and that's not so easy to do. In fact, there isn't a precise equation for calculating the perimeter of any oval other than a circle. However, there are quite a few ways to accurately estimate it. Here is a helper function for quickly getting an ellipse's perimeter distance:

from java.lang import Math
def getOvalCircumference(width, height):
	horizontalRadius = width / 2.0 # Major Axis
	verticleRadius = height / 2.0 # Minor Axis
	
	# Calculate a close approximation of the ellipse's circumference
	# https://www.mathsisfun.com/geometry/ellipse-perimeter.html ~ "Approximation 2"
	return Math.PI * (3 * (horizontalRadius + verticleRadius) - Math.sqrt((3 * horizontalRadius + verticleRadius) * (horizontalRadius + 3 * verticleRadius)))

Armed with that, it is now possible to curve text around an oval using this function:

from java.lang import Math
def drawStringAroundOval(graphics, text, x, y, width, height, degreeOffset=0, isInverted=False):
	centerX = x + width / 2.0
	centerY = y + height / 2.0
	horizontalRadius = width / 2.0 # Major Axis
	verticalRadius = height / 2.0 # Minor Axis
	circumference = getOvalCircumference(width, height)
	
	fontMetrics = graphics.fontMetrics
	
	# Set the unit circle starting angle around the ellips for the string
	angle = Math.toRadians(degreeOffset)
	
	 # If inverted, the top of the characters will face the origin instead of the bottom
	direction = -1 if isInverted else 1
	for character in text:
		# Get the current transform for the graphics object,
		# ...so it can be reset for the next character position calculation
		originalTransform = graphics.getTransform()
		
		# Measure how much horizontal space this character takes in the current font,
		# ...and convert that character width into an angular amount around the cylinder.
		characterWidth = fontMetrics.charWidth(character)		
		widthOffset = (characterWidth / circumference) * 2 * Math.PI * direction		
		angle -= widthOffset / 2.0	# Move halfway into this character's angular space before drawing it to center the character on its respective angle
		
		# Calculate the x and y coordinates for the character on the ellipse, and translate origin to that position,
		# ...so all that has be calculated to draw in the right place are the centering offsets
		characterX = centerX + Math.cos(angle) * horizontalRadius
		characterY = centerY - Math.sin(angle) * verticalRadius
		graphics.translate(characterX, characterY)
		
		# Calculate the tangent direction of the ellipse at this angle, and rotate the character accordingly
		tangentX = -horizontalRadius * Math.sin(angle)
		tangentY = -verticalRadius * Math.cos(angle)
		rotationAngle = Math.atan2(tangentY, tangentX) if isInverted else Math.atan2(tangentY, tangentX) + Math.PI 
		graphics.rotate(rotationAngle)
		
		# Adjust the string position to center it vertically on its ellipse and horizontally on its angle
		centeringOffsetX = -characterWidth / 2
		centeringOffsetY = fontMetrics.ascent / 2
		graphics.drawString(character, centeringOffsetX, centeringOffsetY)
		
		# Restore the graphics transform for the next character.
		graphics.setTransform(originalTransform)
		
		# Add in the second half of the character's width,
		# ...which places its value at the start of the next character's space
		angle -= widthOffset / 2.0

In this script, x, y, width and height are obviously the position and dimension of the oval.
Supplying a degreeOffset will move the string around the ellipse in a counter clockwise direction if a positive value is supplied or a clockwise direction if a negative value is supplied.
I added an optional isInverted boolean flag because I could imagine times when it would be desirable for the top of the text to face inward, but if set to false or ignored, the bottom of the text will face inward.

I imagine that usually, the length of the string will not be long enough to circumvent the given oval, and in those instances, it would be desirable to center the text either on the top or the bottom of whatever is being drawn around. To accomplish this, use fontMetrics to determine the width of the string and use it to calculate what percentage of the oval's circumference is being consumed by the text. The angular offset will be that percentage of 360 degrees. My function always starts at 0 degrees as it appears on the unit circle, and intuitively, a positive offset will always push the text counter clockwise, so whether or not the text is inverted matters, since inverted text will be typed in the opposite direction.

Here is an example of how how to handle either scenario to position the text, so it is centered at the top of the ellipse:

testString = 'Coffee is that savory elixir that transforms complex logic into functional code and abstract ideas into deployed reality'
####
# Calculate position and dimensions here
####
isInverted = False # or True
circumference = event.source.getOvalCircumference(stringOvalWidth, stringOvalHeight)
stringWidth = fontMetrics.stringWidth(testString)
ovalPercentage = float(stringWidth) / circumference
angleOffset = (360 * ovalPercentage) / 2
stringOvalAngleOffset = 90 - angleOffset if isInverted else 90 + angleOffset # Always centered at the top
event.source.drawStringAroundOval(graphics, testString, x, y, width, height, stringOvalAngleOffset, isInverted)

Of course, from here the next natural progression is to wrap text around a cylinder. Here is a library script I developed that does this:

from java.lang import Math
from java.awt import AlphaComposite    # Used to fade the letters slightly at the edges
def drawStringAroundCylinder(graphics, text, x, y, width, height):
    fontMetrics = graphics.fontMetrics
    
    # Make it impossible for any characters to overlap the edges of the cup
    originalClip = graphics.getClip()
    graphics.setClip(x, y, width, (2 * height))
    
    # This is used to slightly widen the major axis of the ellipse,
    # ...so the text won't curve up as much at the edges
    # ...making it easier to squish them around the edges of the cylinder
    flatteningFactor = 1.15
    flattenedWidth = flatteningFactor * width
    
    # The center of the ellipse
    centerX = x + width / 2.0
    centerY = y + height / 2.0
    
    # The radii of the magor and minor axes
    horizontalRadius = flattenedWidth / 2.0 # Modified to be wider than the actual cup
    verticalRadius = height / 2.0
    
    # Get the estinated circumference of the slightly widened oval
    circumference = getOvalCircumference(flattenedWidth, height)
    
    # Starting at the middle of the given string and working outward,
    # ...get the letters that will actually fit or partially fit within the bounds of the cup
    index = 0
    midIndex = len(text) / 2
    finalString = text[midIndex]
    while fontMetrics.stringWidth(finalString) <= width and index <= midIndex:
        index += 1
        startIndex = max(0, midIndex - index)
        endIndex = min(len(text), midIndex + index)
        finalString = text[startIndex:endIndex] # Do this last to ensure it adds two overhanging letters before the loop breaks
    
    # Get the width of the clipped string,
    # ...and caculate the necessary angular offset to center the text on the cylinder
    stringWidth = fontMetrics.stringWidth(finalString) / flatteningFactor
    ovalPercentage = float(stringWidth) / circumference
    angleOffset = 360.0 * ovalPercentage / 2.0
    startAngle = 270.0 - angleOffset
    
    # Start the text on the offset starting angle, and individually draw each character in the circle
    angle = Math.toRadians(startAngle)
    for character in finalString:
        
        # Measure how much horizontal space this character takes in the current font,
        # ...and convert that character width into an angular amount around the cylinder.
        characterWidth = fontMetrics.charWidth(character) / flatteningFactor
        widthOffset = characterWidth / circumference * 2.0 * Math.PI
        angle += widthOffset / 2.0
        
        # Calculate the x and y coordinates for the character on the ellipse
        characterX = centerX + Math.cos(angle) * horizontalRadius
        characterY = centerY - Math.sin(angle) * verticalRadius
        
        # Take the absolute value of the distance from origin
        # ...to determine how close the character is to the left or right edge of the ellipse.
        edgeFactor = abs(characterX - centerX) / horizontalRadius# The value of edge is 0 at the center and approaches 1 at either side.
        
        # Use the proxity from the edge calculate an amount squish the letters horizontally as they approach the edge of the cylinder
        # ...to simulate the letters angling away around the curvature of the cup
        squishFactor = Math.sqrt(max(0.0, 1.0 - edgeFactor * edgeFactor))

        # Save the current graphics state so this character's transparency,
        # ...translation, and scaling do not affect the next character.
        originalTransform = graphics.getTransform()
        originalComposite = graphics.composite        
        
        # Make the characters fade from view as they get closer to the cylinder edge.
        graphics.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, squishFactor)
        
        # Move the drawing origin to the character's ellipse position,
        # ...and apply the squish factor to horizontally squeeze characters that are near the edges.
        graphics.translate(characterX, characterY)
        graphics.scale(squishFactor, 1.0)
        
        # Draw the character centered on the transformed origin.
        centeringOffsetX = -characterWidth / 2
        centeringOffsetY = fontMetrics.ascent / 2
        graphics.drawString(character, centeringOffsetX, centeringOffsetY)
        
        # Restore original composite and transform,
        # ...and increment the angle in preparation for the next cycle
        graphics.composite = originalComposite        
        graphics.setTransform(originalTransform)    
        angle += widthOffset / 2.0
        
    # Restore the drawing and painting ability outside the bounds of the cup
    graphics.setClip(originalClip)

This is similar to curving text around an oval except I've found that not angling the characters produces a more believable effect, and furthermore, some hocus pocus is needed near edges of the cylinder to make the text appear to curve away. Lastly, an ellipse tends to start curving up sharply near its edge, and that looks unnatural when the goal is to curve around to the back instead of up, so I've found that it's better to make the ellipse of the cylinder slightly wider to flatten out the curve a bit.

To demonstrate the effectiveness of this technique, I'll add a fontSize property to the paintable canvas and bind it to the floatValue of a spinner component that I've set up to increment at a value of 0.5
image

I'll package the preceding functions into custom methods that I'll call from the repaint event:

Note: The last two custom methods are used to generate the coffee cup that I will be wrapping with text. I'll provide all of the scripts at the bottom of the post in case anybody has a use for them.

Watch how the text appears to wrap around the edges as the font size increases and the text begins to overflow:
Coffee Cup

Here is the repaint event along with each custom method I developed for this tutorial. Simply click on the arrows to reveal and copy the code:

Paintable canvas repaint event handler:

repaint
graphics = event.graphics
color = system.vision.color # Version 8.3 or newer
#color = system.gui.color # Version 8.1 or older
 
# The coffee cup
cupX = cupY = 100
cupWidth = 140
cupHeight = 180
cupTopOvalHeight = 40
event.source.paintCoffeeCup(graphics, cupX, cupY, cupWidth, cupHeight)

# The large string to be drawn in an oval well outside the bounds of the cup
originalFont = graphics.font
graphics.font = originalFont.deriveFont(16.0).deriveFont(originalFont.BOLD + originalFont.ITALIC)
fontMetrics = graphics.fontMetrics
testString = 'Coffee is that savory elixir that transforms complex logic into functional code and abstract ideas into deployed reality'
stringOvalMargin = 80 # How far away from the outside of the cup, should this oval of text be
stringOvalX = cupX - stringOvalMargin
stringOvalY = cupY - stringOvalMargin
stringOvalWidth = cupWidth + (2 * stringOvalMargin)
stringOvalHeight = cupHeight + (2 * stringOvalMargin)

isInverted = True
circumference = event.source.getOvalCircumference(stringOvalWidth, stringOvalHeight)
stringWidth = fontMetrics.stringWidth(testString)
ovalPercentage = float(stringWidth) / circumference
angleOffset = (360 * ovalPercentage) / 2
stringOvalAngleOffset = 90 - angleOffset if isInverted else 90 + angleOffset
graphics.color = color('black')
event.source.drawStringAroundOval(graphics, testString, stringOvalX, stringOvalY, stringOvalWidth, stringOvalHeight, stringOvalAngleOffset, isInverted)

# The text that is wrapped around the outside of the coffee cup
graphics.font = originalFont.deriveFont(event.source.fontSize).deriveFont(originalFont.BOLD)
nextTestStrings = [unicode('I LOVE ♥'), 'Good Coffee'] # Two lines to print
graphics.color = color(0, 0, 0, 155)
lineSpacing = int(event.source.fontSize * 1.5)
for index, word in enumerate(nextTestStrings):
    event.source.drawStringAroundCylinder(graphics, word, cupX, cupY + cupTopOvalHeight + (index * lineSpacing), cupWidth, cupTopOvalHeight)

Custom Methods on the paintable canvas:

drawStringAroundCylinder
#def drawStringAroundCylinder(self, graphics, text, x, y, width, height):
	from java.lang import Math
	from java.awt import AlphaComposite	# Used to fade the letters slightly at the edges
	fontMetrics = graphics.fontMetrics
	
	# Make it impossible for any characters to overlap the edges of the cup
	originalClip = graphics.getClip()
	graphics.setClip(x, y, width, (2 * height))
	
	# This is used to slightly widen the major axis of the ellipse,
	# ...so the text won't curve up as much at the edges
	# ...making it easier to squish them around the edges of the cylinder
	flatteningFactor = 1.15
	flattenedWidth = flatteningFactor * width
	
	# The center of the ellipse
	centerX = x + width / 2.0
	centerY = y + height / 2.0
	
	# The radii of the magor and minor axes
	horizontalRadius = flattenedWidth / 2.0 # Modified to be wider than the actual cup
	verticalRadius = height / 2.0
	
	# Get the estinated circumference of the slightly widened oval
	circumference = self.getOvalCircumference(flattenedWidth, height)
	
	# Starting at the middle of the given string and working outward,
	# ...get the letters that will actually fit or partially fit within the bounds of the cup
	index = 0
	midIndex = len(text) / 2
	finalString = text[midIndex]
	while fontMetrics.stringWidth(finalString) <= width and index <= midIndex:
		index += 1
		startIndex = max(0, midIndex - index)
		endIndex = min(len(text), midIndex + index)
		finalString = text[startIndex:endIndex] # Do this last to ensure it adds two overhanging letters before the loop breaks
	
	# Get the width of the clipped string,
	# ...and caculate the necessary angular offset to center the text on the cylinder
	stringWidth = fontMetrics.stringWidth(finalString) / flatteningFactor
	ovalPercentage = float(stringWidth) / circumference
	angleOffset = 360.0 * ovalPercentage / 2.0
	startAngle = 270.0 - angleOffset
	
	# Start the text on the offset starting angle, and individually draw each character in the circle
	angle = Math.toRadians(startAngle)
	for character in finalString:
		
		# Measure how much horizontal space this character takes in the current font,
		# ...and convert that character width into an angular amount around the cylinder.
		characterWidth = fontMetrics.charWidth(character) / flatteningFactor
		widthOffset = characterWidth / circumference * 2.0 * Math.PI
		angle += widthOffset / 2.0
		
		# Calculate the x and y coordinates for the character on the ellipse
		characterX = centerX + Math.cos(angle) * horizontalRadius
		characterY = centerY - Math.sin(angle) * verticalRadius
		
		# Take the absolute value of the distance from origin
		# ...to determine how close the character is to the left or right edge of the ellipse.
		edgeFactor = abs(characterX - centerX) / horizontalRadius# The value of edge is 0 at the center and approaches 1 at either side.
		
		# Use the proxity from the edge calculate an amount squish the letters horizontally as they approach the edge of the cylinder
		# ...to simulate the letters angling away around the curvature of the cup
		squishFactor = Math.sqrt(max(0.0, 1.0 - edgeFactor * edgeFactor))

		# Save the current graphics state so this character's transparency,
		# ...translation, and scaling do not affect the next character.
		originalTransform = graphics.getTransform()
		originalComposite = graphics.composite		
		
		# Make the characters fade from view as they get closer to the cylinder edge.
		graphics.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, squishFactor)
		
		# Move the drawing origin to the character's ellipse position,
		# ...and apply the squish factor to horizontally squeeze characters that are near the edges.
		graphics.translate(characterX, characterY)
		graphics.scale(squishFactor, 1.0)
		
		# Draw the character centered on the transformed origin.
		centeringOffsetX = -characterWidth / 2
		centeringOffsetY = fontMetrics.ascent / 2
		graphics.drawString(character, centeringOffsetX, centeringOffsetY)
		
		# Restore original composite and transform,
		# ...and increment the angle in preparation for the next cycle
		graphics.composite = originalComposite		
		graphics.setTransform(originalTransform)	
		angle += widthOffset / 2.0
		
	# Restore the drawing and painting ability outside the bounds of the cup
	graphics.setClip(originalClip)
drawStringAroundOval
#def drawStringAroundOval(self, graphics, text, x, y, width, height, degreeOffset=0, isInverted=False):
	from java.lang import Math
	centerX = x + width / 2.0
	centerY = y + height / 2.0
	horizontalRadius = width / 2.0 # Major Axis
	verticalRadius = height / 2.0 # Minor Axis
	circumference = self.getOvalCircumference(width, height)
	
	fontMetrics = graphics.fontMetrics
	
	# Set the unit circle starting angle around the ellips for the string
	angle = Math.toRadians(degreeOffset)
	
	 # If inverted, the top of the characters will face the origin instead of the bottom
	direction = -1 if isInverted else 1
	for character in text:
		# Get the current transform for the graphics object,
		# ...so it can be reset for the next character position calculation
		originalTransform = graphics.getTransform()
		
		# Measure how much horizontal space this character takes in the current font,
		# ...and convert that character width into an angular amount around the cylinder.
		characterWidth = fontMetrics.charWidth(character)		
		widthOffset = (characterWidth / circumference) * 2 * Math.PI * direction		
		angle -= widthOffset / 2.0	# Move halfway into this character's angular space before drawing it to center the character on its respective angle
		
		# Calculate the x and y coordinates for the character on the ellipse, and translate origin to that position,
		# ...so all that has be calculated to draw in the right place are the centering offsets
		characterX = centerX + Math.cos(angle) * horizontalRadius
		characterY = centerY - Math.sin(angle) * verticalRadius
		graphics.translate(characterX, characterY)
		
		# Calculate the tangent direction of the ellipse at this angle, and rotate the character accordingly
		tangentX = -horizontalRadius * Math.sin(angle)
		tangentY = -verticalRadius * Math.cos(angle)
		rotationAngle = Math.atan2(tangentY, tangentX) if isInverted else Math.atan2(tangentY, tangentX) + Math.PI 
		graphics.rotate(rotationAngle)
		
		# Adjust the string position to center it vertically on its ellipse and horizontally on its angle
		centeringOffsetX = -characterWidth / 2
		centeringOffsetY = fontMetrics.ascent / 2
		graphics.drawString(character, centeringOffsetX, centeringOffsetY)
		
		# Restore the graphics transform for the next character.
		graphics.setTransform(originalTransform)
		
		# Add in the second half of the character's width,
		# ...which places its value at the start of the next character's space
		angle -= widthOffset / 2.0
getOvalCircumference
#def getOvalCircumference(self, width, height):
	from java.lang import Math
	horizontalRadius = width / 2.0 # Major Axis
	verticleRadius = height / 2.0 # Minor Axis
	
	# Calculate a close approximation of the ellipse's circumference
	# https://www.mathsisfun.com/geometry/ellipse-perimeter.html ~ "Approximation 2"
	return Math.PI * (3 * (horizontalRadius + verticleRadius) - Math.sqrt((3 * horizontalRadius + verticleRadius) * (horizontalRadius + 3 * verticleRadius)))
getSpacedOvalPoints
#def getSpacedOvalPoints(self, pointCount, centerX, centerY, width, height, degreeOffset=0):
	from java.lang import Math
	radiusWidth = width / 2
	radiusHeight = height / 2
	
	# java.lang.Math trigonometric functions requires radians
	startAngle = Math.toRadians(degreeOffset) # ...or (3.14 * degreeOffset) / 180 to eliminate the import
	
	# Set the end angle to 360 degrees more than the starting angle
	endAngle = startAngle + (2 * Math.PI) # This could also be written startAngle + Math.toRadians(360)
	
	# Abort the operation if there aren't at least two points to avoid a potential division error and other wierdness
	if pointCount <= 1:
		yield centerX, centerY
		return
	
	# Iterate through each point, and yield a coordinate set for the current angle and distance from origin
	angleIncrement = (endAngle - startAngle) / float(pointCount - 1)
	for point in xrange(pointCount):
		angle = startAngle + (point * angleIncrement)
		yield ( # Polar-to-Cartesian conversions
			int(centerX + Math.cos(angle) * radiusWidth),
			int(centerY - Math.sin(angle) * radiusHeight))
paintCoffeeCup
#def paintCoffeeCup(self, graphics, x, y, width, height):
	from java.awt import BasicStroke
	color = system.gui.color
	
	rimWidth = 4
	cupTopHeight = 40
	cupX = cupY = 0
	cupWidth = 140
	cupHeight = 180
	cupID = cupWidth - (2 * rimWidth)
	cirumference = int(3.14 * cupWidth)
	
	# Allow the cup to be resized, but for simplicity,
	# ...just scale the original development size to the given size
	originalTransform = graphics.getTransform()		# ...so the scaling and origin can be restored after the coffee cup is painted
	graphics.translate(x, y)						# Translate the disired x and y to origin, so the cup doesn't move when scaled
	graphics.scale(float(width) / cupWidth, float(height) / cupHeight)

	lightRGB = 240
	shadowRGB = 180
	grey = color('lightgrey')
	darkGrey = color(120,120,120)
	coffee = color(75,45,25)
	shadow = color(shadowRGB, shadowRGB, shadowRGB)
	highlight = color(lightRGB, lightRGB ,lightRGB)
	
	# Handle
	handleThickness = 18
	handleX = cupX + cupWidth - 10
	handleY = cupY + 45
	handleWidth = 55
	handleHeight = 85
	for strokeWidth in xrange(handleThickness, 0, -1):
		shadowPercentage = float(strokeWidth) / handleThickness
		adjustedRGB = lightRGB - int((lightRGB - shadowRGB) * shadowPercentage)
		alpha = 255 if strokeWidth == handleThickness else 10
		graphics.color = color(adjustedRGB, adjustedRGB, adjustedRGB, alpha)
		graphics.stroke = BasicStroke(strokeWidth)
		graphics.drawOval(handleX - 20, handleY, handleWidth, handleHeight)
	
	
	# Added to eliminate aliasing at large size,
	# ...and to smooth out the bottom after cup highlighting
	graphics.color = grey
	graphics.fillRect(cupX, cupY + (cupTopHeight / 2), cupWidth, cupHeight - cupTopHeight)
	graphics.fillOval(cupX, cupY + cupHeight - cupTopHeight + 1, cupWidth, cupTopHeight)
	
	# Create the body of the cup with vertical lines in a light to shadow arc
	centerX = cupX + (cupWidth / 2)
	centerY = cupY + (cupTopHeight / 2)
	for index, (x, y) in enumerate(self.getSpacedOvalPoints(cirumference, centerX, centerY, cupWidth, cupTopHeight)):
		if index > cirumference / 2:
			shadowPercentage = float(index - cirumference / 2) / (cirumference / 2)
			adjustedRGB = lightRGB - int((lightRGB - shadowRGB) * shadowPercentage)
			graphics.color = color(adjustedRGB, adjustedRGB, adjustedRGB)
			graphics.drawLine(x, y, x, y + cupHeight - cupTopHeight)
	
	# Inside visible part of the cup
	graphics.fillOval(cupX + (rimWidth / 2), cupY, cupWidth - rimWidth, cupTopHeight)
	
	# Cup Rim
	graphics.stroke = BasicStroke(rimWidth)
	graphics.color = grey
	graphics.drawOval(cupX + (rimWidth / 2), cupY, cupWidth - rimWidth, cupTopHeight)
	
	# Coffee Surface
	graphics.color = coffee
	liquidInset = 6
	levelDrop = -8
	graphics.fillOval(cupX + liquidInset, cupY - levelDrop, cupWidth - (2 * liquidInset), cupTopHeight - (liquidInset + rimWidth))
	
	# Coffee spot
	spotX = 30
	spotY = 12
	spotWidth = 30
	spotHeight = 8
	graphics.color = color(130,90,60)
	graphics.fillOval(cupX + spotX, cupY + spotY, spotWidth, spotHeight)
	
	# Steam - S-shaped curls
	steamTopY = cupY - 18
	curlSpacing = curlOneX = cupID / 4
	curlTwoX = curlOneX + curlSpacing
	curlThreeX = curlTwoX + curlSpacing
	steamHeight = 28
	steamWidth = 22
	steamThickness = 6
	graphics.color = color(130,90,60, 50).brighter() #highlight
	graphics.stroke = BasicStroke(steamThickness)
	for steamX in [cupX + curlOneX, cupX + curlTwoX, cupX + curlThreeX]:
		# top curve
		graphics.drawArc(steamX, steamTopY - 22, steamWidth, steamHeight, 270, 140)
		
		# bottom curve
		graphics.drawArc(steamX, steamTopY + steamThickness, steamWidth, steamHeight, 114, 140)
	
	# Restore original graphics positioning and painting parameters
	graphics.setTransform(originalTransform)