- Published on
Creating a Rotating Knob Figma Plugin
Figma - Rotating Knobs
Whilst designing UI’s for Kontakt I found process for building custom knobs in Kontakt is to import an image containing every frame of rotation of a knob, stacked. This is a little frustrating to manually do, and the accepted solution appears to be to use the knobman web tool to handle the process, but isn’t easy for more complicated designs, as well as being a pretty 1998-feeling website. I started creating my knobs in Figma, with all of the rest of the design, but the frame process became a bit of a slog. I’d create the knob, and then add it to a frame with an auto layout. Then I could duplicate the knob and rotate it, letting the auto layout handle the stacking. This works, but is arduous - for a couple of reasons. In lots of cases, you don’t want the entire knob to rotate - for example, if shadow is applied to the knob, rotating the entire knob will rotate the shadow, and as light source isn’t changing, it looks odd. So, you only really want to rotate the indicator on the knob, however if the indicator is a separate element, then you need to make it pivot around the centre point of the knob itself. To achieve this, I created an invisible copy of the main circle on the knob and grouped it and the indicator. Then I could rotate this group and it would pivot the indicator no problem. Using the properties editor instead of the mouse was quicker and more precise for making frames, so I utilised that, and built my first Figma knob. 55 frames long, and I knew I didn’t want to do it again for different knobs down the road.
Plugins
So I decided to look into programming Figma plugins. There’s an easy boilerplate, installable through the Desktop app, that initialises a new plugin project with TypeScript. Installing dependencies and type definitions is enough to get going building a new plugin. Once edited, the .ts
file needs to be compiled, and then the Figma Desktop app will be able to find it and run the plugin.
I began by making a super simple plugin that just set up page templates, containing pages for briefs, typography, colours, components etc. In a few minutes, I had my very first Figma plugin. Pretty neat. So the next step was to learn about interacting with the document. This is kind of tricky, and seems like there’s not tons of info around yet on the nuances of it, so plenty of time reading the docs was required. It is straightforward to clone a selection, allowing the user to select their knob, run the plugin, and it can create the copies. I appended each copy to a frame which had the auto layout turned on, so they began to stack neatly. Then I just needed to figure out the rotation.
Rotation
By default, groupNodes in Figma have a rotation property that can be set, but the point of rotation is not the center of the group. Instead it rotates around the relative origin for the selection. For a rotation, the graph would look like so:
The docs say when rotating about the central point, use the relativeTransform
property, and pass it an Affine Transformation that handles the rotation. They also provide the formula for rotation with an Affine Transformation:
Because my knowledge of matrices and trig was pretty rusty, I went back to learn how this matrix is derived (https://www.youtube.com/watch?v=OYuoPTRVzxY).
This achieves the same result as setting the rotation
property, rotating it around the wrong point. So in order to do the rotation around the central point, you have to do something a bit funky. You need to first translate the point from the original location to the (0,0)
point, apply the normal rotation formula, and then translate it back by the opposite vector used for the initial translation. The Figma docs shows the following matrix for performing a translation:
Where and . My first thought was that I could apply the translation as the relativeTransform
first, then set the rotation after, followed by the final translation. However, because we are simply setting a property of the target, the first translation is just overwritten as soon as you set the rotation. You need to multiply the translation by the rotation, and then the result by the final translation to combine all steps into one Affine Transformation.
Digging back over matrix multiplication, I filled several pages of a notebook before arriving at an answer and finding that my transformation was skewing the target, not just rotating. Solving gave me the full transformation matrix that I needed to use to perform rotation.
Only… this rotated the selected group, but also translated away from its origin point. So now I’m trying to figure out what I need to do to get the node to end up in the same spot after rotation. But another thought has just occurred. I don’t actually need to translate it back again, I can just perform the initial translation to the centre, do the rotation, and then I’ll be appending the rotated shape to a different frame anyway, so the position won’t matter. The critical point is that the rotation happens around the central point. It make prove trick when it comes to performing the rotation just on a ‘spin’ layer, but I’ll cross that bridge when I get there.
Rotation problem
Currently, when I rotate the knob, the bounding box is forcing the movement away from the previous element and the top of the frame. To solve this, I could try and not use the auto layout, but instead just put them in position manually based on an increment and an offset of their width.
I am using a gutter to make sure there is space between each element, and that is being applied correctly, and the value seems to be correct in terms of spacing, but the value is being skewed. This must still be to do with somehow not rotating around the centre point.
Thanks to Luke Finch’s gist, found the code he used to rotate around the centre.
https://gist.github.com/LukeFinch/d3c93d79a9dcd6970358be1d17838318
let angle = 45
let theta = angle * (Math.PI / 180) //radians
let sel = figma.currentPage.selection[0]
//cx,cy is the center of the node
let cx = sel.x + sel.width / 2
let cy = sel.y + sel.height / 2
let newx =
Math.cos(theta) * sel.x +
sel.y * Math.sin(theta) -
cy * Math.sin(theta) -
cx * Math.cos(theta) +
cx
let newy =
-Math.sin(theta) * sel.x +
cx * Math.sin(theta) +
sel.y * Math.cos(theta) -
cy * Math.cos(theta) +
cy
sel.relativeTransform = [
[Math.cos(theta), Math.sin(theta), newx],
[-Math.sin(theta), Math.cos(theta), newy],
]
Positioning the Knobs
The knobs are now rotating correctly, I just need to also position them equally, and also add them into a group, or let the user do it. The first element should be an exact copy of the selection, then the rotation begins from there. Each element is pushed further along. This would mean element one is 0, element two is , element three is …
That’s what I get with that formula. Because the width of the bounds increases as the circle rotates, it means it gets pushed inside the previous knob if left at 100. What makes it trick is that when I access the width property, it is still 100, as the transformation is applied as a property, but the width doesn’t seem to be updated based on that transformation. Got it. Was able to spread the knobs out first, then apply the relative transformation once the new coordinate had been set.
The Rotation Amount
The user should be providing the start position of the knob, and I should figure out the end position. The end position should simply be the mirror image of the start, but that might be tricky to figure out. Either way, I think that’s a task for tomorrow.
Added the parameters
Figma allows plugins to run with user-accepted parameters. They can be defined in the manifest like so:
{
"name": "Knob Rotator",
"id": "1150213363013997713",
"api": "1.0.0",
"main": "code.js",
"editorType": ["figma"],
"parameters": [
{
"name": "Degrees",
"key": "degrees",
"allowFreeform": true
},
{
"name": "Frames",
"key": "frames",
"allowFreeform": true
}
]
}
Here I have added to parameteres, degrees
and frames
. degrees
allows the user to define how far they want the knob to rotate. The frames define over how many frames they want that rotation to occur. So if you wanted a full rotation over 30 frames, you'd enter 360 for the degrees
and 30 for the frames
. I was also able to provide suggested values for the parameters.
This then gives the user an easy way to input information without having to build out my own UI for the plugin (overkill for such a small amount of input).
Parent hierarchy spacing
Then, I needed to get the alignment set up. For Kontakt, the supplied image must have perfectly equal spacing so the total size split into the number of frames specified gets each individual frae perfectly. To export the generated knobs, the easiest way for the user is to have them grouped, and export the entire group as one image. I set up the groups, only to discover that the necessary alignment options aren't available for groups with the API. So, I needed to group the knobs all into one group, and then put that group into a frame, at which point I can set the alignment of the frame and the internal group to make sure it has the correct vertical and horizontal padding. Then set the frame opacity one 0, and it's ready to generate a neat set of knobs.
Levelling up
The next level to take this to would be for the user-selection to be able to have a rotatable element, but not spin the entire selection - i.e spinning the pointer on the knob, but not the entire knob itself. I also noticed a number of people asking about a slider version of this type of plugin, so perhaps I'll build that one next.