I work a lot in Figma, and often times I find myself with images with single-colored backgrounds. It’s a bit frustrating because Figma doesn’t have native support for background removal [update: it is getting one soon!], and the only way to make up for it are plugins. The few popular ones all use some kind of online services, like remove.bg or the Icons8 Background Remover. They are great plugins, but they seem to have minds of their own because the results can sometimes be abysmal. They also come with limits and a price tag, and while I’m not against paying for a service, I find it a bit ridiculous to have to pay for something that should be a basic feature in a design tool. And Photoshop takes too long to open, I’m very lazy.
Another problem is that background removal is a bit too smart for its own good. It’s great that it can detect the background and remove it, but sometimes it can be a bit too aggressive and remove parts of the image that I want to keep. I’ve had to manually fix the images so many times that I’ve lost count. Here’s a demonstration with two random pictures I found in my Downloads folder:
What we need isn’t background removal, but simply replacing or removing a color. Sounds easy enough.
But gang, how do we make a Figma plugin?
I’ve never made a Figma plugin before, but from what I had heard, it was simple JavaScript and some HTML, so this plugin should be done in a few hours.
Or so I thought.
It seemed pretty straightforward at first — open the Figma desktop app and it’ll create a set of files to get you started. The template was a plugin that opened a window and made a few squares.
Digging around through the Typescript and HTML files, the plugin had a UI (ui.html
) that would communicate with the code.ts
file. I could handle all the logic in code.ts
, and send the output to the UI to display. Unfortunately, the Figma plugin environment is built a bit differently. To quote from their website:
For performance, Figma uses an execution model where plugin code runs on the main thread in a sandbox. The sandbox is a minimal JavaScript environment and does not expose browser APIs. This means that you have all of standard JavaScript ES6+ available, including the standard types, the JSON and Promise APIs, binary types like Uint8Array, etc. But browser APIs like XMLHttpRequest and the DOM are not directly available from the sandbox.
The sandbox environment had very minimal JavaScript, which meant that I had to do all the heavy lifting in the UI. It wouldn’t make a significant difference in performance due to the fact that the entire plugin runs on the Figma client, but it was a bit annoying to have to write all the logic in the UI. But this was complex for one other reason — the UI couldn’t communicate with Figma directly. I had to send messages to the code.ts
file, which would then communicate with Figma.
Now, this was already way more complicated than I had anticipated for something so simple, and TypeScript wasn’t making it any better. Using code directly from the examples on their website gave type errors and ESLint warnings. At one point, I decided to switch to JavaScript but that was even worse because I had to deal with the lack of intellisense, it was like being blind.
Even worse, my initial implementation of this idea using the canvas API didn’t work. My plan was to load the image, loop through each pixel, check if the color of the pixel was the target color, and then replace it with the necessary color. I still don’t know why it didn’t work. The code was perfect in theory and the console threw no errors. It just didn’t work. And I really wasn’t feeling the vibes there.
With all of that, I gave up. Even ChatGPT gave up.
Round two, fight!
My parents didn’t raise a quitter, so I decided to give it another shot. I started to have a few ideas during the time I wasn’t working on this. What if I let someone else do the image processing? That way I could focus on communicating between the UI and Figma. I didn’t want to use an online service (because then the plugin wouldn’t work offline), but instead rely on a tried and tested library.
I found replace-color which did exactly the same. It replaced a color in an image with another color. The ingenuity of this package, however, was that it simply did not just replace colors. It could also replace colors that were similar to the target color, so that the output would be much cleaner. This was based on the Delta E algorithm, which calculates the difference between two colors. The lower the difference, the more similar the colors are. It was insane science, and I was all for it.
Delta E | Perception |
---|---|
0 - 1 | Not perceptible by human eyes |
1 - 2 | Perceptible through close observation |
2 - 10 | Perceptible at a glance |
10 - 49 | Colors are more similar than opposite |
100 | Colors are exact opposites |
There was one problem, though. The library was built for Node.js, which we knew for sure wouldn’t work in the Figma plugin environment. It turns out replace-color was built on top of Jimp, a popular image processing library. The best part — it was written entirely in JavaScript, with zero native dependencies, which meant that I could use it in the Figma plugin environment. This also meant that I had to convert replace-color to work in the browser, as the rest of the JIMP code could handle itself. Looking at the code, it seemed like it was just a few changes to make it work in the browser, with replacing module.exports
with browser-friendly code. It seemed like something I could do manually, but it turned out quickly that was not the case. The plugin seemed to use additional dependencies, which led to a larger dependency tree, which I couldn’t manually convert to browser code.
This brought forward one of the most fundamental problems in web development — how do you allow require
in the browser?
Transpiling
Transpiling is the process of converting code from one language to another. It’s a common practice in web development, where you convert TypeScript to JavaScript, or SCSS to CSS. In this case, I wanted to convert Node.js code to browser-friendly code. I wasn’t very knowledgeable on this front, but a quick Google search told me about Browserify. Browserify is a tool that allows you to use require
in the browser. It seems to do some more magic than that, but that’s the gist of it.
I made sure to follow the instructions and everything, but when I ran the Browserify CLI command, I kept getting this annoying error:
Error: Can't walk dependency graph: Cannot find module [module name]
I would install the module specified in the error, and it would reappear with a different module’s name. I’ve heard of not being able to walk your dog, but this was my first time of hearing about not being able to walk dependency graphs.
So I decided to look for a different tool. Browserify looked like it hadn’t been updated in a while, and so I decided to go with one of the best in the business — Webpack. Although Webpack is officially a bundler, it can also do some transpiling to bundle everything up together. They say old is gold, and it couldn’t be truer when it comes to Webpack. While most people have moved on to newer tools like Vite or Snowpack, Webpack still has a special place in my heart because of its bulkiness and slowness compared to lightnight-fast Usain Bolt-esque tools like Vite.
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./index.js", // Your package entry point
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
library: "replaceColor", // Global variable for UMD
libraryTarget: "umd", // Universal Module Definition
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // Do not transpile dependencies
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"], // Transpile to browser-compatible JS
},
},
},
],
},
resolve: {
fallback: {
// Add browser-compatible polyfills for Node.js features if required
},
},
};
With that, I ran the Webpack command. To my surprise, it worked on the very first try! I now had a 600 KB browser-compatible version of replace-color, which I could use in my Figma plugin. I was ecstatic, and I couldn’t wait to test it out.
If you ever need a browser (and minified) version of replace-color, I gotchu.
GitHub: https://github.com/khalby786/chromakey/blob/main/scripts/replace-color.js
CDN: https://cdn.jsdelivr.net/gh/khalby786/chromakey/scripts/replace-color.js
Back to Figma
I decided to start from scratch. Created a new Figma plugin, and started from the ground up. Conveniently enough, I found an official tutorial on image manipulation using Figma plugins. It was similar to what I wanted to implement and it significantly made things easier. Even then, TypeScript would throw tantrums despite me using officially documented and vetted code (I hope no one at Figma is writing tutorials off the top of their heads) but I would @ts-expect-error
my way through it. To be frank, I’d never heard of @ts-expect-error
before, but ESLint suggested it over @ts-ignore
and I’d give anything to get rid of those annoying squiggly red lines.
I tried referencing our new browser-compatible replace-color in the Figma plugin, but it didn’t work. Script tags couldn’t find the file, and using the the fetch
API to load the script and evaling it didn’t work. Disheartened, I decided to ask for some help.
Enter Gavin
5 years ago, I was joining random servers for free Nitro giveaways. Now, I was joining random servers for help with my code. I was truly proud of how far I’d come in life. There was a Discord server mentioned in the docs, and I decided to join it. As someone who has joined way too many servers for help with my code, I tried the trick of searching through the messages to see if anyone had the same issue. But unfortunately, no one had the same issue, so I had to be the savior for the future generations.
Within a few minutes, someone called Gavin replied. Gavin was kind enough to inform that all my scripts had to be in the main UI file for some undisclosed reason. He recommended I use a bundler, but I still hadn’t recovered from using Webpack, and I certainly wasn’t going to spend more time with it to bundle a HTML file of things. Gavin then recommended I just put all the scripts inline, which he reassured me was a common practice, and I’ll admit, that hadn’t crossed my mind. It might be a bit messy and difficult to work with (but it was just one file), but it certainly would work well.
Final touches
After that, it was just a matter of sending Uint8Arrays back and forth, and properly communicating with the UI. The actual magic took place in the UI, where I would recieve the Figma image as a Uint8Array, convert it to an image buffer, then run it through our browser-friendly replace-color. The output would then be sent back to Figma, and voilà!
I was particularly proud of my image format detection code to set the appropriate mime type. It was nothing too complex, but I was just happy I was able to implement my compsci major knowledge somewhere.
// Find the image format by looking at first few bytes
// And then set the appropriate mime type
var imageFormat;
if (
bytes[0] === 137 &&
bytes[1] === 80 &&
bytes[2] === 78 &&
bytes[3] === 71 &&
bytes[4] === 13 &&
bytes[5] === 10 &&
bytes[6] === 26 &&
bytes[7] === 10
) {
imageFormat = "image/png";
} else if (bytes[0] === 255 && bytes[1] === 216) {
imageFormat = "image/jpeg";
} else {
parent.postMessage(
{
pluginMessage: {
type: "error",
message: "Please select a PNG or JPEG file!",
},
},
"*"
);
return;
}
Once I had reviewed the publishing checklist and made fancy banners and demo videos, I was ready to publish!
4 days later
I had accidentally ran Prettier on my HTML file before publishing, and that included the inline minified script which also got prettified and was now 10 times longer. I like to think that that was the reason why it took 4 days for the plugin to be approved. But it was finally approved, and I was ready to share my revolutionary (lmao) plugin with the world.
The plugin is officially called Chroma Key, and it’s available on the Figma Community. It’s free, and it’s open source. I’m not sure if it’s going to be useful to anyone, but I’m proud of it. It’s a bit slow, but it works. And that’s all that matters.