Add fireworks and sparks to a UIView

Do you like a little things that happens in apps you’re using? I looked at dribbble for inspirations and found a beautiful design of onboarding process where fireworks exploded (!) around important view when user changes settings or something. I wondered how difficult it is to implement, and some time later I finished it :)

Fireworks in details

Here is a detailed description of the effect. Fireworks should explode on a specific position around a view - presumably a button reacting on tap event. When it is tapped fireworks should explode close to the corners of the button and explosion should shift sparks from the explosion origin on their own trajectories.

Love it! Makes my eyes happy and makes me want to tap the button all the time :) 🎉

Now let’s take a look at the animation. Behaviour of firework as a whole is similar for every generated firework. There are also small differencies in the trajectories and scale of sparks. Let’s break it down.

There are two fireworks on each button tap,

Each of them consists of 8 sparks,

Each spark follow its own trajectory,

Trajectories look similar but they’re not identical. Few goes to the right, few to the left, others to up and down looking from the explode origin standpoint.

Sparks distribution

This firework has a simple spark distribution. Two sparks on each “view quarter” around the explosion origin. Two sparks in the top right, bottom right, bottom left and top left quarters.

Spark trajectory

Spark has a trajectory it is moving on. There is 8 sparks in a single firework so there is 8 trajectories needed at least. Ideally there should be more trajectories so we can random, so consecutive fireworks don’t look exactly as previous ones.

I’ve created 4 trajectories for each quarter to add some randomness - twice more than sparks. For simplicity of calculations I normalized position of points creating trajectories. Presented trajectories differs a bit from what I finished with because I’ve used different tool to visualize implemented trajectories - but you can get the idea :)

Implementation

Enough theory. Let’s put all the pieces together.

protocolSparkTrajectory{/// Stores all points that defines a trajectory.varpoints:[CGPoint]{getset}/// A path representing trajectory.varpath:UIBezierPath{get}}

Here is a protocol representing spark trajectory. Sharing common interface will make it easier to create various types of trajectories. I’ve decided to go with trajectories based on Cubic Bézier curve and added an init method so I can make it one line call. Path of this type must consists of four points. First and last points specifies start and finish position and two middle points are points to control a curvature of a path. You can use [demos] online math tool to play with Bézier curve.

/// Bezier path with two control points.structCubicBezierTrajectory:SparkTrajectory{varpoints=[CGPoint]()init(_x0:CGFloat,_y0:CGFloat,_x1:CGFloat,_y1:CGFloat,_x2:CGFloat,_y2:CGFloat,_x3:CGFloat,_y3:CGFloat){self.points.append(CGPoint(x:x0,y:y0))self.points.append(CGPoint(x:x1,y:y1))self.points.append(CGPoint(x:x2,y:y2))self.points.append(CGPoint(x:x3,y:y3))}varpath:UIBezierPath{guardself.points.count==4else{fatalError("4 points required")}letpath=UIBezierPath()path.move(to:self.points[0])path.addCurve(to:self.points[3],controlPoint1:self.points[1],controlPoint2:self.points[2])returnpath}}

Moving on. Next thing to implement is a factory that can random a trajectory. On the drawing above you can see trajectories grouped by color. I’ve only created top right and bottom right trajectories and mirror them. This is fine for a firework we’re about to launch 🚀

I’ve named this firework a classic firework. Added a protocol for abstract spark trajectory factory, and a protocol for classic spark trajectory factory to be able to replace the classic factory with another factory.

As I mentioned before there are trajectories for two quarters I’ve created using desmos tool, and later on we’ll ask for a trajectory in a specific quarter top or bottom right.

Important notice: The y-axis* values are flipped, so if desmos tool presents positive value on Y-axis you should flip it to negative because of coordinates system in iOS - lower value closer the top of the screen.

Also it is worth to mention that for simplicity of later calculation each trajectory starts at (0, 0).

We have trajectories now. Let’s create a visual representation of a spark. Spark for classic firework will be a circle that has a color. Keeping the implementation more abstract will allow to create different spark views, e.g. with ducks images, or with pusheen cats in no time :)

You can see that implementation of spark that look like pusheen cat should be easy. Let’s build a classic firework.

typealiasFireworkSpark=(sparkView:SparkView,trajectory:SparkTrajectory)protocolFirework{/// Defines origin of firework.varorigin:CGPoint{getset}/// Defines trajectory scale. Trajectory is normalized so it needs to be scaled up/// before presenting on screen.varscale:CGFloat{getset}/// Defines size of a single spark.varsparkSize:CGSize{getset}/// Returns trajectoriesvartrajectoryFactory:SparkTrajectoryFactory{get}/// Returns spark viewsvarsparkViewFactory:SparkViewFactory{get}funcsparkViewFactoryData(atindex:Int)->SparkViewFactoryDatafuncsparkView(atindex:Int)->SparkViewfunctrajectory(atindex:Int)->SparkTrajectory}extensionFirework{/// Helper method that return spark view and corresponding trajectory.funcspark(atindex:Int)->FireworkSpark{returnFireworkSpark(self.sparkView(at:index),self.trajectory(at:index))}}

Here is a abstract of a firework. To instantiate a firework what it needs:

origin,

scale,

sparkSize

trajectoryFactory

sparkViewFactory

Before we go to classic implementation there is this concept of scaling trajectory that I didn’t mention before. As a spark trajectory is normalized and values of their points are close to <-1, 1> or similar, we want them to be proper size when spark is following the path. We have to scale the path up to cover bigger part of a screen. Also, we need to be able to flip a path horizontally so we can get trajectories for a left part of a classic spark, and be able to shift entire path a bit in specified direction (just for randomness and better control). Here are two methods that help us achieve the goal. I believe it’s self-explanatory.

extensionSparkTrajectory{/// Scales a trajectory so it fits to a UI requirements in terms of size of a trajectory./// Use it after all other transforms have been applied and before `shift`.funcscale(byvalue:CGFloat)->SparkTrajectory{varcopy=self(0..<self.points.count).forEach{copy.points[$0].multiply(by:value)}returncopy}/// Flips trajectory horizontallyfuncflip()->SparkTrajectory{varcopy=self(0..<self.points.count).forEach{copy.points[$0].x*=-1}returncopy}/// Shifts a trajectory by (x, y). Applies to each point./// Use it after all other transformations have been applied and after `scale`.funcshift(topoint:CGPoint)->SparkTrajectory{varcopy=selfletvector=CGVector(dx:point.x,dy:point.y)(0..<self.points.count).forEach{copy.points[$0].add(vector:vector)}returncopy}}

What it does it takes a spark tuple which is a spark view and its trajectory as well as animation duration. Whatever happens in the method is up to us. My implementation is quite big but it just do three things: make the spark view follow the trajectory, scales it up and down (with some randomness), and finally changes it’s opacity. Simple. Also because of keeping this SparkViewAnimator abstract we can simply use different animators as we wish.

With the code presented we should be able to present firework on a specific view. I took it one step further and created a ClassicFireworkController that will manage all the work and let us make a single call to launch a firework.

This firework controller do one more thing. It can change a zPosition of a firework, so if we have a button it can launch some sparks behind and ahead of it for better look.

classClassicFireworkController{varsparkAnimator:SparkViewAnimator{returnClassicFireworkAnimator()}funccreateFirework(atorigin:CGPoint,sparkSize:CGSize,scale:CGFloat)->Firework{returnClassicFirework(origin:origin,sparkSize:sparkSize,scale:scale)}/// It allows fireworks to explodes in close range of corners of a source viewfuncaddFireworks(countfireworksCount:Int=1,sparkssparksCount:Int,aroundsourceView:UIView,sparkSize:CGSize=CGSize(width:7,height:7),scale:CGFloat=45.0,maxVectorChange:CGFloat=15.0,animationDuration:TimeInterval=0.4,canChangeZIndex:Bool=true){guardletsuperview=sourceView.superviewelse{fatalError()}letorigins=[CGPoint(x:sourceView.frame.minX,y:sourceView.frame.minY),CGPoint(x:sourceView.frame.maxX,y:sourceView.frame.minY),CGPoint(x:sourceView.frame.minX,y:sourceView.frame.maxY),CGPoint(x:sourceView.frame.maxX,y:sourceView.frame.maxY),]for_in0..<fireworksCount{letidx=Int(arc4random_uniform(UInt32(origins.count)))letorigin=origins[idx].adding(vector:self.randomChangeVector(max:maxVectorChange))letfirework=self.createFirework(at:origin,sparkSize:sparkSize,scale:scale)forsparkIndexin0..<sparksCount{letspark=firework.spark(at:sparkIndex)spark.sparkView.isHidden=truesuperview.addSubview(spark.sparkView)ifcanChangeZIndex{letzIndexChange:CGFloat=arc4random_uniform(2)==0?-1:+1spark.sparkView.layer.zPosition=sourceView.layer.zPosition+zIndexChange}else{spark.sparkView.layer.zPosition=sourceView.layer.zPosition}self.sparkAnimator.animate(spark:spark,duration:animationDuration)}}}privatefuncrandomChangeVector(max:CGFloat)->CGVector{returnCGVector(dx:self.randomChange(max:max),dy:self.randomChange(max:max))}privatefuncrandomChange(max:CGFloat)->CGFloat{returnCGFloat(arc4random_uniform(UInt32(max)))-(max/2.0)}}

This controller does few things. It random a corner of a button to present a firework at. It adds some randomness to firework origin and create requested number of fireworks and sparks. Then it adds sparks to a source view, adjusts zIndex if possible and run the animator.

Almost all parameters have default value so you don’t need to care about them. You can call the controller just with this:

self.fireworkController.addFireworks(count:2,sparks:8,around:button)

And voilà!

From this point it is easy to get a new type of firework that works like the following one. You just need to define new trajectories, create new firework and implement them to launch sparks as you wish. Putting all this code together in a controller simplifies launching fireworks wherever you want :) Or you can use a fountain firework I’ve included in tomkowz/fireworks on github.

Wrap up

Wasn’t easy but wasn’t difficult too. With proper analysis of the problem (or effect in that case) we can break it down to small pieces and put it together one piece at a time. Hopefully I’ll have a chance to use this effect in a future project 🎉