Happy Independence Day!

Easy enough
Thanks, I modeled what I had onto that example, and this was the result.
Here is my animations.fireworksShow library script in case anybody wants to use it or develop it further:
from java.awt.geom import GeneralPath
from java.awt import BasicStroke
from java.lang.Math import sin, cos, PI
color = system.gui.color
fireworksShow = [ # startFrame, burstX, burstY, hueColor(r, g, b), sparkCount, radius
(0, 120, 145, (255,210,90), 70, 105),
(18, 285, 130, (80,170,255), 65, 90),
(35, 430, 250, (255,90,90), 80, 120),
(52, 500, 140, (110,255,120), 70, 105),
(68, 645, 95, (255,245,210), 95, 150),
(82, 715, 205, (255,210,90), 85, 130),
(100, 815, 165, (80,160,255), 75, 110),
(116, 960, 275, (255,120,90), 80, 125),
(132, 1040, 145, (255,210,90), 72, 110),
(148, 1160, 230, (110,255,120), 68, 100)]
def drawFireworksShow(graphics, frame, startFrame, launchX, launchY, burstX, burstY, hueColor, sparkCount, radius):
# Some dynamic pproperties such as rgba values (o - 255) have to be contained no matter what within a specific range to be valid
# ...so this helper function is used as a clean way to contain such things
def getClampedValue(value, low, high):
return max(low, min(high, value))
# -X^2 + 2X + 1
# Use a quadradic equation to create a parabala
# Not just for the geomotry, but also for the changes speed and fade
# Fireworks start fast and bright when they burst, but then they slow and become gradually dimmer
def getArcFactor(value):
value = getClampedValue(value, 0.0, 1.0)
return 1 - (1 - value) * (1 - value)
# Creates a factor that can be used to create seemingly random patterns that will always repeat
def getVariationFactor(sparkIndex, startFrame, rangeSize):
# Create a seemingly random, but repeatable, index value
firstMultiplier = 37
secondMultiplier = 91
pseudoRandomIndex = sparkIndex * firstMultiplier + startFrame * secondMultiplier
# Wrap the value into the desired range.
wrappedIndex = pseudoRandomIndex % rangeSize
# Normalize to a factor between 0.0 and 1.0.
return wrappedIndex / float(rangeSize)
# ============================
# These hard coded parameters could be abstracted in some way to create more variation in the display
launchDuration = 32 # How many frames from launch to explosion
explosionDuration = 95 # How many frames does the explosion last
initialFlashDuration = 4 # How long does the initial explosion birst last before it fades away
maximumDrop = 45 # Simulates gravity --> using a parabolic drop factor,
# ...the position of each spark will be offset in pixels by this total over the life cycle
frictionFactor = 0.985 # Simulates air resistance by slowing the spark down a bit each frame
repeatInterval = 180 # How often in frames each firework repeats
rocketDiameter = 6 # The width of the rocket
trailingDotDiameter = 4 # The width of the rocket tail
initialBurstDiameter = 24 # The size of the initial explosion before the streamers become visible
# Burst colors
whiteHot = (90, 170, 255)
gold = (110, 255, 120)
orangeRed = (255, 80, 65)
blueGreen = (255, 245, 210)
# ===========================
# Notes and observations on "age"
# The modulus will keep the cycle between 0 and the repeat interval
# The timer counts up forever, so it is unlikely that the frame minus the startFrame will ever be negative,
# ...but if this happens, the age will end up being the repeat interval minus the frame
# The repeat interval should be significantly larger than the combined launch and explosion durations,
# ...so the fireworks don't suddenly disappear and start over or repeat so quickly that it becomes noticable
age = (frame - startFrame) % repeatInterval
# Abort the operation if the modulus puts the firework outside the launch and explosion windows
if age > (launchDuration + explosionDuration):
return
# Rocket launch / trace
if age < launchDuration:
# Get the launch percentage and offset it parabolically,
# ...so it moves fast at first, but eventually slows down and runs out of steam toward the end of it's launch
launchPercentage = age / float(launchDuration)
launchArcFactor = getArcFactor(launchPercentage)
# Calculate the current frame location for the rocket using the parabolically offset percentage
x = launchX + (burstX - launchX) * launchArcFactor
y = launchY + (burstY - launchY) * launchArcFactor
# Calculate the circle radii for coordinate offsetting
# ... to center the rocket and trailing dots on the calculated coordinates
rocketRadius = rocketDiameter / 2
dotRadius = trailingDotDiameter / 2
# Paint the bright rocket head
graphics.color = color(255, 245, 210, 230)
graphics.fillOval(int(x - rocketRadius), int(y - rocketRadius), rocketDiameter, rocketDiameter)
# Paint the fading launch trail
trailingDotCount = 8
for trailingDot in xrange(trailingDotCount):
# Reduce the percentage of each dot by an equally spaced amount
# ...based upon the percentage if the launch distance travelled
regressionFactor = 0.035
dotOffset = max(0.0, launchPercentage - trailingDot * regressionFactor)
# Calculate the x and y coordinates using the offset percentage
# ...and coupled with the parabolic offset that causes the projectile to slow at the end of its arc,
# ...which also causes the dots to come closer together making the slowing down more appearant
dotX = launchX + (burstX - launchX) * getArcFactor(dotOffset)
dotY = launchY + (burstY - launchY) * getArcFactor(dotOffset)
# Make each progressive dat more transparent, so it looks like the trail of projectile is getting cooler the further away
alpha = int(160 * (1.0 - trailingDot / float(trailingDotCount)))
graphics.color = system.gui.color(255, 170, 80, alpha)
# This could be made to taper a bit,
# ...but the decreased transparency somehow makes the dots look progressively smaller even though they aren't
# ...so the added complexity seems unnecessary
graphics.fillOval(int(dotX - dotRadius), int(dotY - dotRadius), trailingDotDiameter, trailingDotDiameter)
# No need to continue past this point during the launch phase
return
# Explosion
explosionAge = age - launchDuration
explosionPercentage = explosionAge / float(explosionDuration)
maxAlpha = 240
alphaBase = int(maxAlpha * (1.0 - explosionPercentage))
expansion = getArcFactor(explosionPercentage)
# Paint the initial burst flash
if explosionAge < initialFlashDuration:
# As each progressive frame of the initial burst passes, ...slowly fade it away
flashPercentage = explosionAge / float(initialFlashDuration)
initialAlphaPercentage = 1.0 - flashPercentage
initialTransparancy = 180
flashAlpha = int(initialTransparancy * initialAlphaPercentage)
graphics.color = color(255, 255, 235, flashAlpha)
initialBurstRadius = initialBurstDiameter / 2
graphics.fillOval(int(burstX - initialBurstRadius), int(burstY - initialBurstRadius), initialBurstDiameter, initialBurstDiameter)
# Individually draw each spark of the given spark count
for spark in xrange(sparkCount):
# Evenly space each spark around a 360 degree (2PI radian) circle
angle = (PI * 2) * (spark / float(sparkCount))
# Shake things up a bit with some pseudo randomness
# The range variables were tested with a couple of numeric text fields
# ...passing the integer values into this function as arguments
# The life range has the most immediate effect, pushing the sparks into haphazard spirals with just a one or two value change
# ...36 to 40 looked the best to me. I liked 38 because it gives the explosion an almost unpredictable eliptical squish
speedRange = 44
lifeRange = 38
minimumSpeedFactor = 0.65
minimumLifeFactor = 0.70
speedFactor = getVariationFactor(spark, startFrame, speedRange) + minimumSpeedFactor
lifeFactor = getVariationFactor(spark, startFrame, lifeRange) + minimumLifeFactor
# This factor makes it where the sparks will not all age at the same speed
sparkAgeFactor = getClampedValue(explosionPercentage / lifeFactor, 0.0, 1.0)
# Simulated acceleration + friction feel
distance = radius * speedFactor * getArcFactor(sparkAgeFactor)
distance = distance * (frictionFactor + (1.0 - frictionFactor) * (1.0 - sparkAgeFactor))
# Gravity offset ~ ...and since the sparkAgeFactor is squared, the effect is parabolic
# ...meaning the drop starts small and moves faster with time
drop = maximumDrop * sparkAgeFactor * sparkAgeFactor
# Alternate curl directions, so neighboring sparks to not curve the same way
curlDirection = -1 if spark % 2 == 0 else 1
# Using pseudo variation, calculate the amount of curl for the current spark at this given frame
minimumCurl = 8
maximumAdditionalCurl = 18
curlVariation = getVariationFactor(spark, startFrame, maximumAdditionalCurl)
curlAmount = (minimumCurl + curlVariation * maximumAdditionalCurl) * sparkAgeFactor
curlOffsetX = -sin(angle) * curlDirection * curlAmount
curlOffsetY = cos(angle) * curlDirection * curlAmount
# Calculate the spark position along its trajectory using the curlOffset
sparkX = burstX + cos(angle) * distance + curlOffsetX
sparkY = burstY + sin(angle) * distance + drop + curlOffsetY
# Calculate the streaming tail coordinates of the spark
initialTailFactor = 0.70
tailStretch = 0.18
tailGravityFactor = 0.55
tailDistance = distance * (initialTailFactor - tailStretch * sparkAgeFactor)
tailX = burstX + cos(angle) * tailDistance
tailY = burstY + sin(angle) * tailDistance + drop * tailGravityFactor # The tail is smokey and should drop slower than the spark itself
# Dramatically offset the transparancy intermittently to create a sparkling effect
if spark % 5 == frame % 5:
flickerFactor = 0.45
else:
flickerFactor = 1.0
alpha = int(alphaBase * (1.0 - sparkAgeFactor) * flickerFactor)
# If alpha hits 0, we wouldn't see the spark even if we wanted to, so it's simply time to move on to the next spark
if alpha <= 0:
continue
# Transparency is variable, so unpack the color variations,
# ...and combine them with the dynamic alpha to create the final color for this iteration , gold, red, blue/green accents
if spark % 13 == 0:
r, g, b = whiteHot
elif spark % 11 == 0:
r, g, b = gold
elif spark % 7 == 0:
r, g, b = orangeRed
elif spark % 3 == 0:
r, g, b = blueGreen
else:
r, g, b = hueColor
graphics.color = system.gui.color(r, g, b, getClampedValue(alpha, 0, 255))
# Gradually shrink the width of the spark and trail as the explosion ages
minimumSparkWidth = 1.0
maximumSparkWidth = 3.0
width = max(minimumSparkWidth, maximumSparkWidth - sparkAgeFactor * 2.0)
graphics.stroke = BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)
# Draw the spark tail
path = GeneralPath()
path.moveTo(float(tailX), float(tailY))
path.quadTo(
float((tailX + sparkX) / 2.0 - sin(angle) * curlDirection * 5),
float((tailY + sparkY) / 2.0 + drop * 0.15),
float(sparkX),
float(sparkY))
graphics.draw(path)
# Paint a glowing spark at the head of the spark tail
size = 3
if spark % 12 == 0:
size = 5
graphics.color = system.gui.color(255, 245, 220, int(alpha * 0.9))
graphics.fillOval(int(sparkX - size / 2), int(sparkY - size / 2), size, size)
# Add in some snap, crakle, pop at the end of the explosion
if sparkAgeFactor > 0.62 and spark % 4 == 0:
crackleAlpha = int(alpha * (1.0 - sparkAgeFactor) * 1.8)
graphics.color = system.gui.color(255, 255, 240, getClampedValue(crackleAlpha, 0, 255))
graphics.fillOval(int(sparkX + ((spark % 3) - 1) * 4), int(sparkY - 2), 3, 3)
I'm calling it from the paintable canvas's repaint event handler with this script:
graphics = event.graphics
frame = event.source.frame
edgeOffset = 50
launchX = event.width / 2 # Center the launch point on the canvas
launchY = event.height + edgeOffset # Put the launch point slightly below the canvas
for delay, burstX, burstY, rgb, particleCount, radius in animations.fireworksShow:
animations.drawFireworksShow(
graphics,
frame,
delay,
launchX, launchY, # launch point
burstX, burstY, # burst point
rgb,
particleCount,
radius)
If anybody wants to paint the animated flag, simply reference the tutorial I made on flag animations here:
Paintable Canvas Hacks: Flag Animations