I decided today that I would learn a bit of Quartz Composer. I had never touched it before, beyond reading a couple articles and watching a conference talk. The most useful introduction for me was "UI Prototyping with Quartz Composer and Origami" by Pasan Premaratne. It takes you from absolute zero to having built a simpler version to Path's attractive spinout menu with Facebook's Origami.
I recommend you not only read Pasan's post, but also actually follow along! At worst you'll have spent twenty minutes exploring a novel way of doing things. At best you'll have acquired a new tool for your kit and become even more handsome.
Near the end of his post, Pasan laments:
The only downside that I see right now to using Quartz Composer is that if you're prototyping something complex, your composition can get unwieldy and convoluted fairly quickly. In just creating a radial menu with three buttons we have over 20 patches in our composition.
I agree that it can become unwieldy. Here's a snapshot of my small composition:
Those three yellow blocks contain essentially the same code. Each block does not explicitly group its contained patches; they are spacially arranged in a particular region on the canvas. Nothing more. So a yellow block is about as constructive as a source code comment.
These blocks contain the same patches duplicated with slightly different parameters. That is of course a bit offensive to me as a programmer. If possible I would like to clean up that repetition. But I'm not even sure that I can.
Here's the rub. If Quartz Composer lacks tools to abstract away chunks of your composition, then it is little more than a shiny toy. It would be like a programming language that does not support creating functions. But if QC does enable building bigger, reusable units of design, then it is worthy of my attention.
So! Let's learn how to build abstractions in Quartz Composer, together! This is my very first day with QC, so there are going to be some false starts. Bear with me. :)
Macros
Step one is to read and follow along with everything in Pasan's post.
Go do it. I'll be here.
As I went through Pasan's tutorial, I kept seeing mention of a "Macro" feature all over Quartz Composer. If it resembles any other system's macro functionality, that would be one way to reduce complexity in your project.
The next step, then, must be to select the group of patches responsible for one of the buttons and click Create Macro
in the toolbar. This replaces all the patches with a single Macro Patch
. The noodle from Interaction 2
's Drag
outlet is connected to this macro patch, which is a good sign. In fact if you go to the Viewer you should see that nothing has changed.
Double click the macro patch to jump into its definition. Take care to double click in its body, not its titlebar (which renames the patch). We can see that it is almost identical to what we had before. However there is a new patch called Number Splitter
near the top. This must be how Quartz Composer connects the Drag
interaction from outside the macro to the inputs of the animations inside the macro. Observe that Number Splitter
's inlet is green, presumably to indicate that it is special in this way.
If we want to make this macro reusable, we would need to create our own parameters. The button image must be one such parameter otherwise every button will be labeled "A". The best place to start is to delete the image patch which is hardcoded to be ButtonA.png
.
To parameterize the image, right click the Layer
patch. Under Publish Inputs
select the Image
property. QC offers to let you name the parameter differently from the name that the Layer
patch expects, but in this case we can stick with Image
. This turns the inlet green, which matches the Number Splitter
parameter. Here's to hoping we're right!
Clicking Edit Parent
in the QC toolbar leaves the macro and returns to our overall composition. If you look at the macro patch you can see that it now has an Image
inlet. Drag that ButtonA.png
file back into the composition, delete its Layer
, and hook up its outlet to the macro's Image
input.
If all went according to plan, there should be no change in the Viewer. Indeed there are still three spinout buttons labeled A, B, and C.
Here begins one of my false starts. Read on before making changes to your composition.
Let's get rid of the other two buttons and replace them with macros. Copy and paste appears to work just fine on macro patches. Ensure that each of the three macros has its Input
and Image
inlets populated.
If you flip to the Viewer you'll see that there's only one button. The problem is that all three buttons are animating between the same positions at the same times. The other two buttons are hiding below the visible one.
To fix this we need more macro parameters. But to add them, we would need to edit the macro three times, once for each button. This is because we used copy and paste to duplicate the macros. Just like in programming, copy and paste is a worst practice.
Lesson learned. False start over.
User-Defined Patches
Copying and pasting macros didn't pan out. How else can we achieve the abstraction we want?
Another button in the QC toolbar is Add to Library
. I presume that is for reusing the components you have created across different projects. Let's add the macro to our library as a new patch type. After playing around a little I've discovered that rather than adding the macro to your library directly, it's better to explode the macro first (available under its right-click menu). Call the new patch type Radial Button
.
Then from the Patch Library drag in two more Radial Button
s and wire them up. You'll notice that the inlets are now called Enable
, Input
, and Input
. That's downright ridiculous, so let's fix that problem first.
In the Patch Library, right click the Radial Button
object and select Edit
. Then without selecting a patch, open up the Patch Inspector. This inspects Radial Button
itself. In the dropdown at the top select Published Inputs & Outputs
. You'll see a table of Input
mapped to input_proxy_1
and another Input
mapped to input_proxy_2
. Change one of the Input
labels to Progress
and the other to Image
. I don't know if there's a way to immediately see which is which. However if you save and reopen Radial Button
, the label on the image layer should say Image
not Progress
. If you guessed wrong be sure to flip them the other way.
We've renamed the properties, so let's go back to our composition to see our change.
Ah crud. Still two uselessly-named Input
inlets. I bet that every time
we edit Radial Button
we must remove it from our composition and add
the new version back in. What a pain! If you know a better solution,
please get in touch. Otherwise, if you really must remove and re-add your custom patches after each change, it's probably best to finish the patch in isolation before adding it to your project.
Beware! Don't forget to adjust the layer ordering any time you add new layers. Hit Area
should be the layer with the highest number, then the Add Button
layer should be the next layer below that. If you miss this step, you will see rendering bugs. Or worse, the touch handler mysteriously won't fire, because it is obscured by other layers.
Next let's make more properties into parameters. x-
and y-coordinate
are as good a place to start as any. Recall that in Pasan's post we assigned different End Value
s to each button's Transition
patches. However, notice that the Start Value
and End Value
of the Transition
patch have ordinary inlet ports. That means we could publish those inputs from Radial Button
itself. Start by right clicking Transition X
, selecting Publish Inputs
, then End Value
. Call it End X
. Similarly, publish Transition Y
's End Value
as End Y
.
Go back to your composition, remove the Radial Button
patches, then
re-add them. You'll see that you have the new End X
and End Y
inputs. Hook up the Progress
and Image
inlets as before. Then, using the same method as in Pasan's post, assign constant values using the Patch Inspector for each of the End X
and End Y
inputs on each of the three buttons. For convenience they are:
- A button:
(-184.5, -408.5)
- B button:
(0, -298.5)
- C button:
(184.5, -408.5)
Notice as you're editing how the default values for End X
and End Y
are the values that had been assigned to each End Value
of Button A
's two Transition
patches. You should change them to better defaults (like 0
) by editing the Radial Button
patch. Use the Patch Inspector with no patch selected, then change the values under Input Parameters
.
In the Viewer, confirm that the animation is working again. The only thing left to fix is the friction and tension of each of the buttons coming out. I'm sure you can handle that.
How Bout Dem Abstractions
Thanks to our custom Radial Button
patch, there is less duplication in our composition. The number of patches has decreased by half, which makes the design more comprehensible.
That is not good enough.
If we want to add or remove a button in this menu, it'd be a surprising amount of work. We would need to do some trigonometry for each button to produce a new set of magic numbers. No way! It needs to be as easy to add a button as it would be in, say, Interface Builder.
Before we attempt that, let's simplify the problem first. Let's move everything to the origin. Change the Radial Button
's Start Value
for both Transition X
and Transition Y
to 0
. And then in your composition, change the y-coordinate
of the Add Button
and the Hit Test
from -512
to 0
.
Using the origin, instead of the magic y-coordinate
-512
, reduces the fiddliness of the task. Also, we'll lay the buttons across the complete circle around the add button, rather than just half or a quarter of it. Once we have that working, coming back and restoring these constraints would be straightforward.
To begin, we'll remove the End X
and End Y
published inputs from Radial Button
. You remove them the same way you added them: just right click the patch and select the property under Published Inputs
.
The new inputs we'll want are Radius
, Count
, and Index
. The Radius
input will tell us how far from the origin to move that button. Count
and Index
will be used to decide where on the circle that button will go.
To calculate the destination of each button, we'll need to use sin
and cos
. Quartz Composer provides a Mathematical Expression
patch that evaluates expressions of arbitrarily many variables. We'll need one patch for the x-coordinate
and another for the y-coordinate
, so drag out two.
If you inspect a Mathematical Patch
, under the Settings
pane there is a text field for formula. Any free variables in this formula will end up as inputs to the patch (which is a wonderful bit of design).
For the x-coordinate
patch, we'll want to use the formula sin(360 * index/count) * radius
Note! sin
uses degrees not radians. Knowing that will save you the
twenty minutes of head-scratching and intense self-doubt that I suffered. :)
For the y-coordinate
patch, we'll use the same formula but with cos
instead, producing cos(360 * index/count) * radius
Hook the Result
outlets of these two patches up to the End Values
of the corresponding Transition
patches.
We'll need to publish the Index
, Count
, and Radius
properties. If we do that on one of the two Mathematical Expression
patches, the other one wouldn't receive those inputs. If we do it on both patches, we might expect that Quartz Composer would send the same value to both patches, but that's not the case. The patch just publishes two sets of inputs with the same names. Useless!
What we want here is an Input Splitter
. Right click one of the Mathematical Expression
patches and select Insert Input Splitter
for Radius
. This gives you a tiny patch with an unnamed input and output. The key to using this is that you can send output from one patch as input to multiple other patches. So drag the noodle from the Radius
Input Splitter
to both Mathematical Expression
patches'Radius
input. Repeat for Index
and Count
.
Then finally we can publish the Radius
, Count
, and Index
properties in the usual way from the Input Splitter
patches.
With all those changes made, our Radial Button
patch looks like this:
And then our project can use that new and improved version like this:
With each Radius
set to 200
, Count
set to 3
, and Index
from 0
to 2
, we get the following result:
Great! We can factor out the Friction
parameter in the same way. (This is your cue!)
Put Your Abstraction to Work
That was certainly a lot of work to build out that Radial Button
patch. Let's see how well it serves us by adding a fourth and fifth button to the menu.
First, add your new images to the composition. Delete their Layer
patches. Drag in two more Radial Button
s. Hook up their Image
and Progress
inlets.
Then set the Radius
of the two new patches to 200
. Set the Count
of all the Radial Button
patches to 5
. Then finally set the Index
values of the new patches to 3
and 4
.
Success!
If you wanted to, you could use an Input Splitter
to avoid duplicating the Radius
and Count
properties and make it even easier to add buttons.
I wonder if there's an automatic way to specify the Count
and Index
properties. Is there a way to count or enumerate the number of connections from an outlet? Quartz Composer provides a lot of patches so I would not be surprised if it did.
To ace this project, you could move the menu to the bottom left of the screen. Make it cover only a quarter of the circle like Path does. I imagine you could implement the latter merely by choosing interesting values for Index
and Count
. Or, better yet, expose the 360
factor from the sin
and cos
formulae as a published input.
The Rub
Quartz Composer is certainly an interesting tool to have in your repertoire. Origami does a lot to make it more usable and more flexible. For future projects I no longer have a reason to prototype animations in code. Origami helps you design your interactions faster and gets you closer to that sweet, sweet instantaneous feedback.
Yes, it is too easy to create a mess of your composition with too many patches. Just like not using functions would make a mess of your code. Quartz Composer thankfully does provide designers robust tools for creating and reusing abstractions. You just have to think like a programmer. ;)
Update: Jeremy Bishop has taken this a couple steps further. He added more parameters and took a stab at the iterator idea.