image

This article is about my experience building a Figma plugin. Making the plugin was surprisingly easy and rich with creative potential. This article is also an opportunity for me to share my thoughts on the importance of extensible software, and what it means for software to be dynamic and user defined. Although I find extensibility - it’s importance, and how to achieve it - a fascinating subject, I discovered that I need to do a lot more research before I can effectively describe my own experiences with this dimension of software.

Repo

But I do know that Figma fits into this experience. When I first used Figma (Design), I was surprised at how intuitive it was to learn. Later, when opening up Figma’s whiteboarding program, FigJam, I was amazed at how fun it was. I would urge my colleagues to hop into a whiteboard with me, and I would look forward to presenting my cloud architecture suggestions.

image

As someone who has been drawing my whole life, I felt at home on a blank expanse of canvas in a way I never had in digital “document”. On a more social level, it was also really interesting to see people interact in 3D space, and watch my colleagues play around with shapes in real time. It was the closest I’ve gotten to playing videogames at work. By playing the exception, it has also served as a sort of reminder of how much computers often isolate us.

Allegory

There was a time in college when I carried a small, thin notebook everywhere. I would doodle in it to pass the time, and I would use to create my todos for the the day, referencing it frequently when I was waiting in line, or had a few spare moments. Sometimes referencing todos turned into a more reflective process, where I would write out details for an art project, or things I wanted to tell a friend.

What I found was that people would stop and ask me what I was doing all the time. In line for food, on benches, in public transit. It was constant. People seemed to be both interested enough and unconcerned enough about crossing any sort of assumed privacy boundary that they would start up conversations all the time.

And I loved it. It was very refreshing. Especially because I would often enjoy sharing these short snippets of my life, my day, and my psyche. I rarely wrote anything too personal in those notebook - more tasks and ideas.

It was so obvious to contrast this to writing notes in a phone that I quickly realized that this never happened when using a phone to do the exact same thing.

I’ve used FigJam in particular for a wide range of personal projects, including:

Planning a Canoeing trip image

Visualizing Binary Trees image

Creating product roadmaps image

Laying out the electrical system, layout, and build process for my camper van conversion. image

image

image

Note

I haven’t blogged about my camper van conversation yet, but I plan to.

So I can attest to the power of the base features of Figma as a way to fluidly explore ideas. Like many modern applications, Figma can be extended with plugins. In order to be available from within the app, each plugin most go through a review process. But they can still be developed locally without that being necessary.

My colleague Seth Walker told me that he was building a Figma plugin, I was interested what that process would be like. I have built a very simple Chrome Extensionbefore, as well as an Obsidian Plugin, and in both cases I felt that the development experience taught me a lot about the internal workings of the parent software. In a very real way, plugins allow users to participate in the building process of software. Because I appreciate Figma software, I was curious how it worked.

image

On Figma Plugins, and Plugins in General

I want to provide a quick primer to Figma plugins if you haven’t seen them. Here’s a video of me exploring a couple out of the 3000 plugins available:

Ultimately, I think that many of the plugins that exist in Figma are too niche, or marketing ploys or experiments, or are too silly to really be useful. I’m not sure why this is. But despite this, the potential availability of the perfect plugin is reassuring, and tends to make me trust Figma more because it means the creators are invested in extensibility.

I read an article the other day about Dynamic Documents as Personal Softwarethat described how software is often rigid, designed to do a single purpose. I feel the writers captured a very important truth when they wrote:

“How might we reorient computing so that people can deeply tailor software to meet their unique needs?”

Inkandswitch.com

The authors of this article contrast the idea and rigidity of a note-taking app as an example of the app in general with the flexibility of a paper note.

One humble but ubiquitous vision of what flexibility in software looks like is the settings panel:

image

Settings panel in Obsidian

But the configurations available in an app’s settings are rarely foundational to the functionality of the app itself. image

Users of the settings panel are beholden to a discrete and limited set of options exposed to them by a developer. That’s why I tend to think of applications with a plugin ecosystem as having a fundamentally different ethos than those without. I trust their approach more.

This is far from a complete reflection on the dichotomy between dynamicand static programs, because this vocabulary is new to me. I think the distinction is exciting, and provides language for experiences I’ve had using different tools that inspired me as a developer (whether it was by way of frustration, or delight).

Figma has provided some of the tools that made me subtly rethink what was possible in software, which is why I’m thinking about these things.

Creating a Figma Plugin

Here is the sample code Figma provided Seth and I with when we got started:

// This file holds the main code for plugins. Code in this file has access to
// the *figma document* via the figma global object.
// You can access browser APIs in the <script> tag inside "ui.html" which has a
// full browser environment (See https://www.figma.com/plugin-docs/how-plugins-run).
 
// Runs this code if the plugin is run in Figma
if (figma.editorType === 'figma') {
  // This plugin will open a window to prompt the user to enter a number, and
  // it will then create that many rectangles on the screen.
 
  // This shows the HTML page in "ui.html".
  figma.showUI(__html__);
 
  // Calls to "parent.postMessage" from within the HTML page will trigger this
  // callback. The callback will be passed the "pluginMessage" property of the
  // posted message.
  figma.ui.onmessage =  (msg: {type: string, count: number}) => {
    // One way of distinguishing between different types of messages sent from
    // your HTML page is to use an object with a "type" property like this.
    if (msg.type === 'create-shapes') {
      const nodes: SceneNode[] = [];
      for (let i = 0; i < msg.count; i++) {
        const rect = figma.createRectangle();
        rect.x = i * 150;
        rect.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}];
        figma.currentPage.appendChild(rect);
        nodes.push(rect);
      }
      figma.currentPage.selection = nodes;
      figma.viewport.scrollAndZoomIntoView(nodes);
    }
 
    // Make sure to close the plugin when you're done. Otherwise the plugin will
    // keep running, which shows the cancel button at the bottom of the screen.
    figma.closePlugin();
  };
}
 
// Runs this code if the plugin is run in FigJam
if (figma.editorType === 'figjam') {
  // This plugin will open a window to prompt the user to enter a number, and
  // it will then create that many shapes and connectors on the screen.
 
  // This shows the HTML page in "ui.html".
  figma.showUI(__html__);
 
  // Calls to "parent.postMessage" from within the HTML page will trigger this
  // callback. The callback will be passed the "pluginMessage" property of the
  // posted message.
  figma.ui.onmessage =  (msg: {type: string, count: number}) => {
    // One way of distinguishing between different types of messages sent from
    // your HTML page is to use an object with a "type" property like this.
    if (msg.type === 'create-shapes') {
      const numberOfShapes = msg.count;
      const nodes: SceneNode[] = [];
      for (let i = 0; i < numberOfShapes; i++) {
        const shape = figma.createShapeWithText();
        // You can set shapeType to one of: 'SQUARE' | 'ELLIPSE' | 'ROUNDED_RECTANGLE' | 'DIAMOND' | 'TRIANGLE_UP' | 'TRIANGLE_DOWN' | 'PARALLELOGRAM_RIGHT' | 'PARALLELOGRAM_LEFT'
        shape.shapeType = 'ROUNDED_RECTANGLE'
        shape.x = i * (shape.width + 200);
        shape.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}];
        figma.currentPage.appendChild(shape);
        nodes.push(shape);
      }
 
      for (let i = 0; i < (numberOfShapes - 1); i++) {
        const connector = figma.createConnector();
        connector.strokeWeight = 8
 
        connector.connectorStart = {
          endpointNodeId: nodes[i].id,
          magnet: 'AUTO',
        };
 
        connector.connectorEnd = {
          endpointNodeId: nodes[i+1].id,
          magnet: 'AUTO',
        };
      }
 
      figma.currentPage.selection = nodes;
      figma.viewport.scrollAndZoomIntoView(nodes);
    }
 
    // Make sure to close the plugin when you're done. Otherwise the plugin will
    // keep running, which shows the cancel button at the bottom of the screen.
    figma.closePlugin();
  };
};
 

I appreciate the comments that are included.

You can easily generate this code and get started by following the quickstart guide, which essentially tells you to right click in Design or FigJam and select Plugins > Development > New Plugin.

image

Figma recommends you build in typescript, just because you interact with many Figma types. Make sure you build your typescript into javascript, and then run your plugin by selecting it in the development menu we just used. If it was the last one run, you can also use the hotkey option + command + p (on mac) to run it again without having to navigate through the menu.

image

Figma plugins don’t need to have UI, and can execute javascript once upon initiation, but I want to be able to listen for key events and send them to our running javascript process, for which I need to have an html file called ui.html:

The Goal

After helping my colleague build Conway’s Game of Life in Figma, I thought “huh, Figma is sort of like a game engine”, probably because many of my programming and design challenges have come from making games. My first thought was whether I could create a “character” that could be moved around the screen with the WASD keys.

I cannibalized Seth’s code, grabbed a rectangle creation line, subscribed to some keypress event listeners, and got that rectangle moving around the screen using user input.

In order to do that I needed some HTML - it’s possible to create Figma plugins that only execute Javascript, but after reading the documentation, it looked like I could only listen to key events through a special file called ui.html rather than the javascript file listed in the plugin manifest. Here it is:

<button id="startBtn">Start</button>
<script>
	document.getElementById('startBtn').onclick = () => {
	  document.body.focus(); // Ensure the body can receive key events
	  document.body.onkeydown = (event) => {
	    const key = event.key.toUpperCase();
		if ([
		'W', 'A', 'S', 'D', 'U', 'H', 'J', 'K', 'N', 'Z',
		'3', '4', '5', '6', '7', '8', '9']
		.includes(key)){
	      parent.postMessage({ pluginMessage: { type: 'keypress', key } }, '*');
    }
  };
};
</script>

All we’re really doing here is creating a start button, focusing on the document body, and listening for a certain subset of key input events that we post to the window.

After writing the code to move this rectangle around like a sort of 2D cursor, I had an urge to interact with the surrounding FigJam environment, so I added the ability to “paint” boxes of the same kind around the box using the UHJK keys, sort of like WASD, but a directional box creation mechanism.

image

This was cool, and I could recycle the same createRectangle I used to make the cursor rectangle, but it was awkward to paint around the rectangle, so I added the ability to paint at the location of the rectangle.

Then I noticed that the cursor’s z-index was less than the boxes it created, and it got lost behind them. I found no way to directly manipulate z-index in the API reference material, so I rewrote the way I moved my box so that I instead deleted and recreated it for the illusion of movement, making use of how newer objects in Figma are placed one top of older objects. This is the code I ended up with:

let history: RectangleNode[] = []
 
const createRectangle = (x: number, y: number, color: { r: number, g: number, b: number }, cursor = false) => {
  const rect = figma.createRectangle();
  rect.x = x;
  rect.y = y;
  rect.fills = [{ type: 'SOLID', color }];
  if (!cursor){
    history.push(rect)
  } else {
    rect.strokes = [{ type: 'SOLID', color: {r: .5, g: 0.15, b: .15} }];
    rect.strokeWeight = 8;
  }
  return rect;
};
 
const moveRectangle = (xMove: number, yMove: number, box: RectangleNode, step: number, color: any) => {
  let newBox = createRectangle(box.x + xMove*step, box.y + yMove*step, color, true)
  box.remove()
  // figma.viewport.scrollAndZoomIntoView(history);
  return newBox
}
 
const hexToRgb = (hex: string) => {
  const r = parseInt(hex.slice(1, 3), 16) / 255;
  const g = parseInt(hex.slice(3, 5), 16) / 255;
  const b = parseInt(hex.slice(5, 7), 16) / 255;
  return { r, g, b };
};
 
const hexColors = ['#141010', '#cf5f4e', '#e5a16d', '#fae2ac', '#822229', '#524d56'];
const rgbColors = hexColors.map(hexToRgb);
 
let snapCoords = (num: number, interval: number = 50) => {
  return num - (num % interval)
}
 
const handleFigjam = () => {
  let currentColor = 0;
 
  // let box = createRectangle((figma.viewport.center.x - 50), figma.viewport.center.y - 50, rgbColors[currentColor], true);
  let box = createRectangle(snapCoords(figma.viewport.center.x, 50), snapCoords(figma.viewport.center.y, 50), rgbColors[currentColor], true);
  box.resize(100, 100);
 
  const moveBox = (direction: string) => {
    const step = 100;
    const actions: { [key: string]: () => void } = {
      // 'W': () => box.y -= step,
      'W': () => box = moveRectangle(0, -1, box, step, rgbColors[currentColor]),
      'A': () => box = moveRectangle(-1, 0, box, step, rgbColors[currentColor]),
      'S': () => box = moveRectangle(0, +1, box, step, rgbColors[currentColor]),
      'D': () => box = moveRectangle(+1, 0, box, step, rgbColors[currentColor]),
      'U': () => createRectangle(box.x, box.y - step, rgbColors[currentColor]),
      'H': () => createRectangle(box.x - step, box.y, rgbColors[currentColor]),
      'J': () => createRectangle(box.x, box.y + step, rgbColors[currentColor]),
      'K': () => createRectangle(box.x + step, box.y, rgbColors[currentColor]),
      'N': () => createRectangle(box.x, box.y, rgbColors[currentColor]),
      'Z': () => {
        if (history.length){
          let last = history.pop()
          if (last) last.remove()
        }
      }
    };
 
    if (actions[direction]) {
      actions[direction]();
    } else if (/^[3-8]$/.test(direction)) {
      currentColor = parseInt(direction) - 3;
      box = moveRectangle(0, 0, box, step, rgbColors[currentColor])
      // box.fills = [{ type: 'SOLID', color: rgbColors[currentColor] }];
    }
  };
 
  figma.ui.onmessage = (msg) => {
    if (msg.type === 'keypress') {
      moveBox(msg.key);
    }
  };
 
  figma.on('close', () => {
    if (box){
      box.remove()
    }
  });
};
 
if (figma.editorType === 'figjam') {
  handleFigjam();
}
 
figma.showUI(__html__);

Then I added undo! Previously, it was only possibly to undo all of the new squares at once.

I also made sure that the cursor square was removed when the plugin was closed.

Lastly, it was boring to just have one color, so I also added the ability to cycle through colors using the number keys 3-8 (so the right hand could easily use them, instead of the left, which is busy with the WASD keys). I grabbed the colors out of this photo of me and my brother I really like taken after I almost drowned in the ocean:

image

Here’s a video of me playing around with it on top of the photo the colors are based on:

And here’s a little dude I made:

image

This is the process of making said lil’ dude:

I made some…other creatures too. Sorry, I misplaced one of their noggins:

image

It turns out that one awesome thing about making pixel art in Figma is that because the result is a set of RectangleNoes, which can be copied, deleted, rotated and manipulated post-creation in all the same ways objects can normally be manipulated in Figma. They be exported as a PNG simply by using the command + shift + c hotkey.

image

Or as JPGs using the menu or command + shift + e export hotkey.

image

The repo is public, so please feel free to clone/fork it and use it in figma

image

I don’t have time to go through the Figma publishing process right now, but if you find it useful (or don’t!), please let me know. I welcome all feedback.

image

What’s next for this project

  • Implement undo
  • When the box zooms out of the screen, focus the view on the box. I know this is very doable because the example plugin Figma provides does this
    • I found this problem actually a bit tricky to define, and that using the mouse to readjust the view might be a better solution than manually readjusting the view. HOWEVER, I did comment out a line that used a list of the entire history of the plugin to readjust the view, which actually work pretty well, depending on what you are doing.
  • ~~Make sure the moving box that is used like a sort of cursor has a higher z-index than the boxes it makes, so it never gets lost underneath those boxes.
  • Actually getting the boxes to stack along the same grid every time (I tried to do this with the snapCoords method, but it isn’t quite behaving) Immediately after writing this I realized that the snap to coords method was not snapping to the full width of the box.
  • Test out multiplayer
  • Floodfill?
  • Allow the cursor to ‘eyedrop-tool’ the color below it (looked into this and there is no obvious way to do this I could find on first glance)
  • Make a multiplayer game.

Reflections

image

I am reaching for a way to categorize the flexibility of different programs, and I’m struggling to quantify this is a satisfying way. Turns out it’s difficult to categorize software on just two dimensions, but here’s an attempt to show how software with plugin / extension ecosystems stacks against other things. I will leave you with this diagram I made in an attempt to map these dimensions out:

image

image

Thanks for reading!

image

image

image image