# Paintable Canvas Hacks

**URL:** <https://forum.inductiveautomation.com/t/paintable-canvas-hacks/85437>\
**Category:** Ignition\
**Tags:** vision\
**Created:** [February 8, 2024, 3:25pm UTC](https://forum.inductiveautomation.com/t/paintable-canvas-hacks/85437 "2024-02-08T15:25:06Z")\
**Posts on this page:** 1\
**Showing post:** 15

<div class="post-metadata">

**Author:** ![justinedwards.jle](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/justinedwards.jle/32/37854_2.png) [@justinedwards.jle](https://forum.inductiveautomation.com/u/justinedwards.jle)\
**Post date:** [May 29, 2026, 3:03pm UTC](https://forum.inductiveautomation.com/t/paintable-canvas-hacks/85437/15 "2026-05-29T15:03:49Z")

</div>

# Flag Animations

![WavyOldGlory](https://us1.discourse-cdn.com/inductiveautomation/original/3X/b/e/be396e08a0b5dcf2805a3e4642fe9174dee72ade.gif)

The approach in this post will work with just about any flag you can come up with, but for reference, the flag I'm animating was originally created and posted here:

[Holiday Paintable Canvas Challenge: Old Glory](https://forum.inductiveautomation.com/t/holiday-paintable-canvas-challenge/112691/9)

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/5/8/58c74466e561c4892917a8c8168913bfd231ade9.png)

**Prerequisite Knowledge:**  
• A timer will need to be set up with its value property bound to a custom frame property on the paintable canvas  
• Imports are going to be required to make this work, so for efficiency, this technique should be set up in the script library and called from the repaint event  
_ **Reference this post for a detailed tutorial on how implement these two prerequisites:** _  
[Paintable Canvas Hacks: Gear Animations](https://forum.inductiveautomation.com/t/paintable-canvas-hacks/85437/14)

That said, here is a quick tutorial for creating wavy flag animations in Ignition  
**Generating sinusoidal waves:**

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/1/f/1f0bf7989cbe8cab319d55595d26d0518740e442.jpeg)

Amplitude and wavelength _[aka period]_ are the only two things that are needed to draw a consistent sine wave in Ignition

```python
amplitude = 14
waveLength = 240

```

As the line is being formed, simply calculate the fraction of the wavelength that has been traversed by the given x coordinate:

```python
cyclePosition = x / float(waveLength)

```

In the case of our flag, we will want the waves to continuously move horizontally, so it looks like the wave is being pushed by the wind. This is where our timer value comes in. It will be used to generate a phase variable that will shift the x coordinates horizontally:

```python
frame = event.source.frame
phase = -(frame % waveLength) # The illusion of the flag being pushed by the wind is created by adjusting the starting point of the sinusoid
												# ...make the phase shift negative to simulate left to right movement
cyclePosition = (x + phase) / float(waveLength)	# Convert the phased x position into its fractional position within the wave cycle

```

To get the y coordinate that corresponds to a given x coordinate in a sine wave, convert the cycle position to radians, take the sin of the resultant value, and multiply it by the amplitude:

```python
y = amplitude * sin(cyclePosition * (PI * 2)) # Evaluate the sine wave at the current cycle position and scale it using the amplitude

```

With a timer value driving the wave, the above code will produce a nice left to right flowing wave of equal amplitude, but that won't work for a flag because one end will be anchored to a pole, and the other end will be free to the wind. Consequently, the result is a dynamic amplitude that is hardly noticeable at one end while appearing quite significant at the other. Therefore, the amplitude will need to be split into a minimum amplitude variable for the anchored end and a maximum additional amplitude parameter that can be scaled according to a given x coordinate's distance from the anchored end.

Putting it together to calculate the x and y coordinates from one end of a wavy flag to the other, it looks like this:

```python
# Animated wave parameters
# ...Note: The side of the flag that is anchored to the pole naturally won't be able to move up and down as much as the side of the flag that's out in the wind,
# ...so the amplitude parameter is split. The amplitude will be ratiod across the flag to from the pole to the free end
# ...with the minWaveAmp at the pole and incrementally added amplitude until all the maxAdditionalAmplitude is added in at the last pixel on the opposite end of the flag
frame = event.source.frame # This is a custom property that's bound to a timer value parameter and is used to drive the flag waving amimation
minWaveAmp = 0.5 # The amount of vertical flag movemnt allowed at the flag pole
maxAdditionalAmplitude = 14 # The amount of additional vertical flag movement that is allowed at the free side of the flag
waveLength = 240 # The math nerd in me wants to call this the period
phase = -(frame % waveLength) # The illusion of the flag being pushed by the wind is created by adjusting the starting point of the sinusoid
											# ...make the phase shift negative to simulate left to right movement
for x in xrange(flagWidth + 1): # Iterate across every pixel along the width of the flag including the final edge
	additionalAmplitude = maxAdditionalAmplitude * (float(x) / flagWidth)	# Calculate what percentage of the maxAdditionalAmplitude to add based on the position moving from the attached end of the flag to the free end of the flag
	totalAmplitude = minWaveAmp + additionalAmplitude # Combine the minimum amplitude with the position based additional amplitude
	cyclePosition = (x + phase) / float(waveLength) # Convert the phased x position into its fractional position within the wave cycle
	y = totalAmplitude * sin(cyclePosition * (PI * 2)) # Evaluate the sine wave at the current cycle position and scale it using the calculated amplitude

```

Here is the full script that was used in the repaint event that generated the depicted flag animation:  
_(Click the right pointing arrow to show the code)_

> **Source Code**
>
> ```python
> from java.awt import BasicStroke
> from java.awt.geom import GeneralPath
> from java.lang.Math import PI, sin
> graphics = event.graphics
> 
> # Flag pole Parameters
> baseRGB = 165 # The darkest shaded portion of the pole
> flagPoleWidth = 20 # 
> flagBallDiameter = 28	# 
> flagGap = 2 # This is the space between the flag pole and the flag (In pixels)
> flagPoleHeight = 500 # An arbitrarily large number that should more than overlap the bottom of the canvas
> 
> # Flag Parameters (In pixels)
> stripeHeight = 14 #
> flagWidth = 345 #
> totalStripes = 13 # The American flag has 13 stripe representing the original 13 colonies
> flagHeight = totalStripes * stripeHeight	#
> cantonWidth = 138 # The technical name for the blue rectangle on the American flag is the canton, but it's also know colloqually as the union
> cantonHeight = stripeHeight * 7 # When this is sized correctly, it covers the first seven stripes
> 
> # Flag Colors
> oldGloryRed = system.gui.color(179, 25, 66)	#
> oldGloryWhite = system.gui.color('white')	#
> oldGloryBlue = system.gui.color(10, 49, 97)	#
> borderColor = system.gui.color(0, 0 ,0, 40)	# Tranparancy creates a nice shadowy effect and minimizes aliasing
> 
> # Animated wave parameters
> # ...Note: The side of the flag that is anchored to the pole naturally won't be able to move up and down as much as the side of the flag that's out in the wind,
> # ...so the amplitude parameter is split. The amplitude will be ratiod across the flag to from the pole to the free end
> # ...with the minWaveAmp at the pole and incrementally added amplitude until all the maxAdditionalAmplitude is added in at the last pixel on the opposite end of the flag
> frame = event.source.frame # This is a custom property that's bound to a timer value parameter and is used to drive the flag waving amimation
> minWaveAmp = 0.5 # The amount of vertical flag movemnt allowed at the flag pole
> maxAdditionalAmplitude = 14 # The amount of additional vertical flag movement that is allowed at the free side of the flag
> waveLength = 240 # The math nerd in me wants to call this the period
> phaseRate = 6 # This could be called windSpeed, and this could also be controlled by simply making the timer count in larger increments than 1
> phase = -(frame % waveLength) * phaseRate	# The illusion of the flag being pushed by the wind is created by adjusting the starting point of the sinusoid
> # ...make the phase shift negative to simulate left to right movement
> 
> # For simplicity, everything is drawn from (0, 0) and the final positions of objects are adjusted via translation
> graphics.translate(flagPoleWidth - flagGap, flagBallDiameter)
> 
> # Paint the shaft of the pole
> for index in xrange(flagPoleWidth):
> rgb = baseRGB + (2 * index) # Adjust the color from dark to bright with each iteration
> graphics.color = system.gui.color(rgb, rgb, rgb) # ...while simultaneously reducing the stroke width
> graphics.stroke = BasicStroke(flagPoleWidth - index,	# ...to create a linear gradient effect
> BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER) # The round cap produces a smoother taper
> x1 = x2 = 0 # X doesn't change on a perfectly vertical line
> y1 = flagPoleWidth - index # This is offset ip to make a tapered neck below the ball
> y2 = flagPoleHeight # The number assigned to this variable needs to exceed the lower edge of the canvas for the bottom of the pole to look right
> graphics.drawLine(x1, y1, x2, y2) # Shift the x position up each iteration to create a point for the ball to rest on
> 
> # Paint the ball on the top of pole
> for index in xrange(flagBallDiameter):
> rgb = baseRGB + (2 * index)
> graphics.color = system.gui.color(rgb, rgb, rgb) # Same as the shaft except with a shinking dot instead of a line
> graphics.stroke = BasicStroke(flagBallDiameter - index, # ...to create a spherical gradient effect that perfectly matches the pole
> BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER) #
> graphics.drawLine(0, -(flagPoleWidth / 3), 0, -(flagPoleWidth / 3))	# The y parameters are offset up, so the ball rests on top of the shaft's upper point
> 
> # Adjust the graphics origin for flag painting
> flagXOffset = (flagPoleWidth / 2) + flagGap #
> flagYOffset = flagPoleWidth # This puts the top of the flag right below the base of the flagPole's cone
> graphics.translate(flagXOffset, flagYOffset)	#
> 
> # All of the math needed to calculate the points for the outline and the canton is done while calculating the points for the stripes,
> # ...so rather than having to do all the calculations again later, the relevent coordinates are simply stored in these empty lists
> # ...and used directly for painting later in the script
> outlinePoints = []
> cantonPoints = []
> 
> # Iterate through the 13 stripes that represent the original 13 colonies and paint them
> for stripeIndex in xrange(totalStripes):
> stripe = GeneralPath()
> 	
> # Alternate red and white
> if stripeIndex % 2:
> graphics.color = oldGloryWhite
> else:
> graphics.color = oldGloryRed
> 
> # Whare the top of the stripe would be without wavyness
> flatStripeY = stripeHeight * stripeIndex
> 	
> # No need to do all the math twice, as the top points are calculated, 
> # ...offset the y coordinate by the stripe height and store it with the x coordinate here for later
> # ...when the bottom points are added to the strip generalPath shape
> bottomPoints = []
> 	
> # Top edge of the stripe
> for x in xrange(flagWidth + 1): # Iterate across every pixel along the width of the flag including the final edge
> additionalAmplitude = maxAdditionalAmplitude * (float(x) / flagWidth)	# Calculate what percentage of the maxAdditionalAmplitude to add based on the position moving from the attached end of the flag to the free end of the flag
> totalAmplitude = minWaveAmp + additionalAmplitude # Combine the minimum amplitude with the position based additional amplitude
> cyclePosition = (x + phase) / float(waveLength) # Convert the phased x position into its fractional position within the wave cycle
> waveOffset = totalAmplitude * sin(cyclePosition * (PI * 2)) # Evaluate the sine wave at the current cycle position and scale it using the calculated amplitude
> wavyStripeY = flatStripeY + waveOffset # Apply the current x position's wave offset to the stripe's base Y coordinate
> bottomPoints.insert(0, (x, wavyStripeY + stripeHeight)) # Add the stripe height to calculated y coodinate for the equivalent x coordinate set on the bottom of the stripe, and insert the resultant set into the bottomPoints list
> 
> # Add all of the top stripe points to the outlinePoints list,
> if stripeIndex == 0:
> outlinePoints.append((x, wavyStripeY))
> 
> # ...and if the x coordinate is one of the ones shared by the canton,
> # ...add that coordinate set to the cantonPoints list
> if x < cantonWidth:
> cantonPoints.append((x, wavyStripeY))
> 
> # Set the general path to its starting point
> if x == 0: # Set the general path to its starting point
> stripe.moveTo(x, wavyStripeY)
> continue
> stripe.lineTo(x, wavyStripeY) # Adding each pixel along the top of stripe from left to right
> 		
> # Bottom edge of the stripe	
> for x, y, in bottomPoints:
> stripe.lineTo(x, y) # Adding each pixel along the bottom of the stripe from right to left
> 
> # Add all the bottom pixels of the bottom stripe to the outlinePoints list
> if stripeIndex == totalStripes - 1:
> outlinePoints.append((x, y))
> 		
> # The bottom of the canton shares its bottom edge with the bottom of the 7th stripe [6th zero indexed iteration],
> # ...so add any coordinate set from the bottom of that strip to the cantonPoints list
> # ...if the x coordinate is left of the canton's right edge
> if stripeIndex == 6 and x < cantonWidth:
> cantonPoints.append((x, y))
> 
> # Complete and paint the stripe
> stripe.closePath()	# Creates the left edge of the stripe by connecting the bottom corner to the top
> graphics.fill(stripe)
> 
> # Create a blue rectangle for the 50 stars
> graphics.color = oldGloryBlue
> canton = GeneralPath()
> startingX, startingY = cantonPoints[0]	# Unpack the fist set of coordinates in the cantonPoints list
> canton.moveTo(startingX, startingY) # ...and add a moveTo iteration to the canton path as a starting point for its iterator
> for x, y in cantonPoints: #
> canton.lineTo(x, y) # Unpack each point in the cantonPoints list and add a lineTo iteration to the canton shape
> canton.closePath() # Close the gap from the bottom left corner to the top left corner
> graphics.fill(canton) # ...and fill the completed shape to paint the canton
> 
> # Define all the points needed to make a single star in the top left corner of the canvas
> points = [
> (4, 0),	# Top outer point
> (5, 3),	# Upper right inner point
> (8, 3),	# Upper right outer point
> (5, 5),	# Lower right inner point
> (7, 8),	# Bottom right outer point
> (4, 7),	# Bottom inner point
> (1, 8),	# Bottom left outer point
> (2, 5),	# Lower left inner point
> (0, 3),	# Upper left outer point
> (3, 3)]	# Upper left inner point
> 
> # Separate the x and y coordinates for the top left star into seperate lists for the fillPolygon method,
> # ...and inset the star by an arbitrary margin (This was originally done directly in the points set,
> edgeMargin = 4
> 
> # Switch to white for the stars
> graphics.color = oldGloryWhite
> 
> # These arbitrary spacing parameters were precisely calculated using trial and error
> horizontalSpacing = 24	# The space betwenn the left edge of a star and the left edge of its neighboring star to the right
> verticalSpacing = 10	# The space between the top edge of a row, and the top edge of the row below it
> 
> # The american flag currently has 5 rows of 6 stars and 4 rows of 5 stars for a total 9 rows
> for row in xrange(9):
> starCount = 5 if row % 2 else 6 # Alternate between rows of 6 and 5 stars
> starInset = 12 if row % 2 else 0	# Inset the rows of 5, so the stars appear evenly staggared	
> 
> # Iterate through each star in the given row,
> # ...and calculate a list of X and Y coordinates for each star to pass into the fillPolygon method
> for star in xrange(starCount):
> starXOffset = starInset + (star * horizontalSpacing)
> 	
> # Use the star's horizontal position to calculate how far the flag wave has moved vertically
> waveOffset = (minWaveAmp + maxAdditionalAmplitude * (float(starXOffset) / flagWidth)) * sin(((starXOffset + phase) / float(waveLength)) * PI * 2)
> 	
> # Create a couple of empty lists to store the calculated wave offset star coordinates in
> starXCoordinates = []
> starYCoordinates = []
> 		
> # Unpack each coordinate set within the current star and calculate the necessary positional offsets
> for xCoordinate, yCoordinate in points:
> 			
> # Shift the star according to whether or not it's in a row with 5 or 6 stars to create a proper staggering effect
> modifiedXcoordinate = xCoordinate + edgeMargin + starXOffset
> 			
> # See the first few lines of the stripe painting loop for a detailed breakdown of the waveOffset calculation
> waveOffset = (minWaveAmp + maxAdditionalAmplitude * (float(modifiedXcoordinate) / flagWidth)) * sin(((starXOffset + phase) / float(waveLength)) * PI * 2)
> 			
> # Factor in all the calculated Y parameters to vertically position the star
> modifiedYCoordinate = int(yCoordinate + edgeMargin + (row * verticalSpacing) + waveOffset)
> 		
> # Add the caclulated coordinates to their respective lists
> starXCoordinates.append(modifiedXcoordinate)
> starYCoordinates.append(modifiedYCoordinate)
> 		
> # Paint the star using the coordinate lists
> graphics.fillPolygon(starXCoordinates, starYCoordinates, len(points))
> 
> # Paint a small border around the flag, so it will look complete against white backgrounds.
> graphics.color = borderColor # Without additional anti aliasing logic, this shape looks badly aliased if it's fully opaque, so the border color should have plenty of transparency
> 
> # Create a general path shape for the outline of the flag
> outline = GeneralPath()
> 
> # Unpack the first coordinate set for use as a starting point for the outline shape
> startingX, startingY = outlinePoints[0]
> outline.moveTo(startingX, startingY)
> 
> # Unpack all coordinate sets in the outlinePoints list,
> # and create a shape that can outline the flag
> for x, y in outlinePoints:
> outline.lineTo(x, y)
> outline.closePath() # Connect the bottom left corner of the flag to the top left corner to complete the outline shape
> 
> # Draw the semitransparent outline to complete the flag
> graphics.stroke = BasicStroke(2)
> graphics.draw(outline)
> 
> ```

...or if preferred, here is an export of the window with the above working example:  
[FlagExample.zip](https://forum.inductiveautomation.com/uploads/short-url/aHJuuzuykHgn2KKudYn9CykPNYI.zip) (14.9 KB)

---

_[View the full topic](https://forum.inductiveautomation.com/t/paintable-canvas-hacks/85437)._
