Developing a Framer Module

John Marstall
BPXL Craft
Published in
7 min readJul 20, 2016

--

Framer Studio is an excellent prototyping tool for designers. Not only does it enable sophisticated prototypes out of the box, but its capabilities can be extended via modules. You’ll find plenty of great modules around the web, but what if you want to build your own?

Unfortunately, there’s a large gap between knowing enough of Framer Studio’s core language, CoffeeScript, to create interactive animations and being able to produce even a simple module. We’ll try to close that gap here.

We’re going to create a module called “GhostLayer” that makes it easier to fade layers interactively. We’ll do this by defining a class that extends the default Layer. This means our class will have all the usual properties of a Layer — you can position it with x and y, give it a drop shadow, etc. — while also enabling some custom functionality we provide.

You can follow along in your preferred text editor or in Framer Studio itself.*

Create a new text file and save it as “GhostLayer.coffee.” We’ll begin by declaring what we’re doing. Type the following into your document:

class GhostLayer extends Layer

Here we’re saying we’re going to create a new class of Layer, named GhostLayer, on the exports object. Because it’s just a kind of Layer, it’ll inherit all the usual capabilities of a layer.

Any time a new entity of type GhostLayer is created, it will be initialized in memory. Anything we want to go into GhostLayer’s initial state needs to be declared in the class constructor. Let’s add that next.

¬ constructor: (@options={}) ->

Note that in CoffeeScript, indentation matters. I’ll be using the symbol ¬ to indicate a level of indentation.

You’re going to see the @ symbol frequently as we build this code. It’s a CoffeeScript shortcut meaning “this.” @options is something of a clipboard we’re reserving for use within the module.

Our GhostLayer needs to be capable of translucency to be of any use. Here’s our constructor in full:

¬ constructor: (@options={}) ->
¬ ¬ @options.translucent ?= false
¬ ¬ super @options

We’ve granted our GhostLayer a new property that layers usually don’t have: that of being translucent. This property is called a boolean in that it can only be either true or false. We set it with ?= to indicate that the user may or may not supply a value. false is our default in that case.

Any other class properties you might add would be inserted within the constructor before super @options. That super @options is what wraps up the initialization and appends our custom features to the layer. Just make sure each property is on its own line and follows the same indentation and syntax.

At the moment translucent doesn’t do anything. We need to establish what effect the property should produce within the constructor — as determined by indentation level.

¬ ¬ if @options.translucent is true
¬ ¬ ¬ @.opacity = 0.5

Remember, @ is equivalent to “this.” Since we’re within the class definition, “this” means the class object itself — the GhostLayer. We’re setting the GhostLayer’s opacity to 0.5 if translucency is turned on.

This feature will allow the user to set the default translucency on creation of a new GhostLayer. She’d type the following code into Framer Studio, which is equivalent to adding it to the prototype’s main app.coffee file (don’t add this to your GhostLayer.coffee document — this is for the user of your module):

GhostLayer = require "GhostLayer"
myGhost = new GhostLayer
¬ translucent: true

However, we might want to let the user toggle translucency at times other than initialization. To do this, we need to define a getter and setter for our translucent property. This is also done outside the constructor but still within the class definition.

¬ @define 'translucent',
¬ ¬ get: ->
¬ ¬ ¬ @options.translucent
¬ ¬ set: (value) ->
¬ ¬ ¬ @options.translucent = value

This is a typical pattern for getters and setters. Note that the string in the @define statement must exactly match the name of the option the getter and setter affects.

Our getter and setter will allow the user to read the current state of the translucent property using

print myGhost.translucent

or change it using

myGhost.translucent = true

But while the property will be updated with a value of true, this will have no visible effect on the GhostLayer. We need to add more code to the setter to make sure the visual results are always synced to the value of the property.

¬ @define 'translucent',
¬ ¬ get: ->
¬ ¬ ¬ @options.translucent
¬ ¬ set: (value) ->
¬ ¬ ¬ @options.translucent = value
¬ ¬ ¬ if @options.translucent is true
¬ ¬ ¬ ¬ @.opacity = 0.5
¬ ¬ ¬ else
¬ ¬ ¬ ¬ @.opacity = 1.0

“This” (the @) in the lexical scope of our module is still the GhostLayer layer object. We’re setting the GhostLayer’s opacity to 50% if translucent is true or 100% if it’s not.

Lexical scope can be a tricky thing to grasp. To look at it a bit more, let’s make our GhostLayer a little more interactive. When the GhostLayer is clicked, we want it to fade to its translucent or opaque state, depending.

Return to the constructor and add an onClick event handler that will trigger a function called fade(). (We won’t require the parentheses here, but you’ll need them if you ever call the function from outside the module.)

¬ constructor: (@options={}) ->
¬ ¬ @options.translucent ?= false
¬ ¬ super @options
¬ ¬ @.onClick @fade

@.onClick means “when this is clicked” — “this” meaning the GhostLayer itself once again.

Below the @define statement and within the class (i.e., indent once), we’ll define our fade() function.

¬ fade: ->

The content of fade() resembles what we provided for the setter above, except reversed; we’re toggling between the translucent and opaque states. Also, we should keep the property boolean in sync with the visual result as we do so.

¬ fade: ->
¬ ¬ if @options.translucent is true
¬ ¬ ¬ @options.translucent = false
¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ opacity: 1.0
¬ ¬ else
¬ ¬ ¬ @options.translucent = true
¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ opacity: 0.5

This will work well enough and produce a nice animated effect. But what if we were to get a bit more complex? Maybe we want to make sure the layer isn’t in the middle of animating already when we trigger the opacity change. After all, that might produce an undesirable flickering effect. Framer provides a mechanism just for this purpose, called debounce. It looks like so:

Utils.debounce 0.5, ->

The time interval (0.5 here for half a second) is sort of a “cooldown.” The function won’t run again until the cooldown has expired, even if it’s triggered in the meantime.

Adding that to fade() looks like so:

¬ fade: Utils.debounce 0.5, ->
¬ ¬ if @options.translucent is true
¬ ¬ ¬ @options.translucent = false
¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ opacity: 1.0
¬ ¬ else
¬ ¬ ¬ @options.translucent = true
¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ opacity: 0.5

Great. But let’s throw ourselves a lexical-scoped curveball. Try introducing a slight delay in the function:

¬ fade: Utils.debounce 0.5, ->
¬ ¬ Utils.delay 0.5, ->
¬ ¬ ¬ if @options.translucent is true
¬ ¬ ¬ ¬ @options.translucent = false
¬ ¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ ¬ opacity: 1.0
¬ ¬ ¬ else
¬ ¬ ¬ ¬ @options.translucent = true
¬ ¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ ¬ opacity: 0.5

Now we have a problem. By making a call out to the global context Utils within the function, we’ve changed our current lexical scope. We’re no longer within the scope of the function. Instead we’ve bounced out to a more global scope that doesn’t know about things like @options.translucent. If you leave the module like this and try to use it in Framer Studio, you’ll get errors about things being undefined.

Fortunately, the solution is simple. We force Framer to maintain the lexical scope by switching one of our arrows to a scope-maintaining “fat” arrow. Look for the => below:

¬ fade: Utils.debounce 0.5, ->
¬ ¬ Utils.delay 0.5, =>
¬ ¬ ¬ if @options.translucent is true
¬ ¬ ¬ ¬ @options.translucent = false
¬ ¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ ¬ opacity: 1.0
¬ ¬ ¬ else
¬ ¬ ¬ ¬ @options.translucent = true
¬ ¬ ¬ ¬ @.animate
¬ ¬ ¬ ¬ ¬ properties:
¬ ¬ ¬ ¬ ¬ ¬ opacity: 0.5

Our fade feature will once again work as expected.

Now, add the following line to the very end to wrap everything up:

module.exports = GhostLayer

exports is another kind of shared clipboard, but one that a prototype housing the module can also access. Any variables or functions living in exports will all be available to the prototype. We want to be able to use the GhostLayer class, so we make sure to reference it here.

That’s enough for a working module. To use it in a prototype, create a new document in Framer Studio. Save the document to a local directory and then open the .framer folder there. Look for the “modules” folder within. That’s where you need to copy our GhostLayer.coffee module. Once you’ve done so, add the following code to your prototype:

GhostLayer = require "GhostLayer"
myGhost = new GhostLayer
¬ translucent: false

(You can use translucent: true if you wish. The layer will just be harder to see.)

Try clicking on the GhostLayer a few times and check the animation. Remember, GhostLayer is just a kind of layer. You can do anything with it that you would do to a normal layer. Feel free to assign a background color, reposition it, etc.

You can download a copy of the module here or the complete prototype here.

Also, if you’re prototyping for Apple TV, we have a nifty RemoteLayer module you should check out.

*Framer Studio stumbles over the exports keyword. If you leave that out, you can write your module in Framer Studio and benefit from its autocompletion and error checking. You will need to add it back in to complete the module. I tend to comment out the exports line during development, then uncomment before I paste the module to its own .coffee file.

For more insights on design and development, subscribe to BPXL Craft and follow Black Pixel on Twitter.

--

--

John Marstall
BPXL Craft

Designer. Xplane | Firewheel Design | Gowalla | Black Pixel | Kaleidoscope | NetNewsWire | Hypergiant