Holiday Paintable Canvas Challenge

Happy Independence Day!

IndependanceDay

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