Flag Animations

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
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
That said, here is a quick tutorial for creating wavy flag animations in Ignition
Generating sinusoidal waves:
Amplitude and wavelength [aka period] are the only two things that are needed to draw a consistent sine wave in Ignition
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:
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:
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:
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:
# 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
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 (14.9 KB)

