この記事が想定している読者
- Figmaでカラーコードを管理していなかったが、管理したいと考えている方
- どれだけのカラーコードが使われているかわからないが、可視化してツッコミをいれたい方
- プロダクトのコードにたくさんのカラーコードがあふれているが、年末年始を迎えようとしている方
月日が流れると管理不能になったカラーコードがあふれている
みなさんも、長年エンジニアをしている中で一度は経験したことがあるのではないでしょうか。
昨今では、デザインシステムを導入し、開発当初からきちんと管理して実装を進めているところも少なくはないと思います。ただ、レガシーとよばれるコード群の中には、管理できなくなったカラーコードがあふれています。「え?灰色の定義多すぎない?」ってこともしばしばあります。
参画するプロジェクトは 「いつもそんな感じだよ」 って方もたくさんいるでしょう。
この記事では、Figma Pluginを利用して足掛かりになるようなアイデアを残したいと思います。
前半と後半にわけて検証したいと思います。
- (前半) ローカルでレポジトリを取得後に解析し、コンソールに出力したSVGテキストをFigmaに反映する
- (後半) Figma Plugin経由で、レポジトリからカラーを抽出してFigma上に反映する
カラリバ...今日は、カラーリバースエンジニアリングのお話です。
(前半) ローカルでレポジトリを取得後に解析し、コンソールに出力したSVGテキストをFigmaに反映する
- 後で、Figma Plugin経由で、レポジトリからカラーを取得する場合に、サーバーレス関数を利用しようと思っているので、
git
は利用せずに、Github-APIを利用してレポジトリの内容を取得しようと思います。
(レポジトリには、bootstrap.min.css
を置いています)
const res = await fetch(
`https://api.github.com/repos/activeguild/plugin-test/zipball/main`
);
- 取得したzipファイルは、unzipperを利用して、展開します。
import * as unzipper from 'unzipper'
...
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const directoryFromBuffer = await unzipper.Open.buffer(buffer);
- 再帰的に、cssファイルを確認して、カラーコードを集計します。
今回は、一部のカラーコードのフォーマットのみに限定しています。(HexColor と RGBの一部)
抽出したカラーコードは、 color-string を利用して、すべてHexColorに変更します。
HexColorに統一しているのは、いずれ、カラーコードもFigma上に反映したいと考えていて、統一されているほうが見やすいと考えているためです。
import { parse, ChildNode } from 'postcss'
import * as cs from 'color-string'
const cssLangs = `\\.(css)($|\\?)`;
const cssLangReg = new RegExp(cssLangs);
const isCSSRequest = (request: string): boolean => cssLangReg.test(request);
const hexColors = new Set<string>()
for (const file of directoryFromBuffer.files) {
if (isCSSRequest(file.path)) {
const fileBuffer = await file.buffer()
for (const hexColor of recursion(parse(fileBuffer.toString()).nodes)) {
hexColors.add(hexColor)
}
}
}
const recursion = (nodes: ChildNode[]): Set<string> => {
const hexColors = new Set<string>()
for (const node of nodes) {
if (node.type === 'rule') {
for (const hexColor of recursion(node.nodes)) {
hexColors.add(hexColor)
}
} else if (node.type === 'decl') {
const { value } = node
const matchedArr = value.match(
/(^#{1}[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$)|(rgba?\( *([+-]?\d*\.?\d+) *, *([+-]?\d*\.?\d+) *, *([+-]?\d*\.?\d+)(?: *, *([+-]?\d*\.?\d+) *)?\)$)/gi,
)
if (matchedArr?.length) {
for (const matched of matchedArr) {
const color = cs.get(matched)
if (color) {
hexColors.add(cs.to.hex(color.value))
}
}
}
}
}
return hexColors
}
- Figmaでは、クリップボードにCopyしたテキストがsvgの書式の場合、ペースト時にsvgに変換して、nodeを作成します。
その機能を利用したいと考えているので、コンソールにSVGのテキストを出力するようにします。
const toSvgAsString = (twoDimensionalHexColors: string[][]) => {
let svgAsString = `<svg
width="${64 * 10}"
height="${64 * twoDimensionalHexColors.length}"
viewBox="0 0 ${64 * 10} ${64 * twoDimensionalHexColors.length}"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>`
for (let i = 0; i < twoDimensionalHexColors.length; i++) {
for (let j = 0; j < twoDimensionalHexColors[i].length; j++) {
svgAsString = `${svgAsString}<rect x="${j * 64}" y="${
i * 64
}" width="64" height="64" fill="${twoDimensionalHexColors[i][j]}" />`
}
}
svgAsString = `${svgAsString}</svg>`
return svgAsString
}
ここで紹介したコードは以下のレポジトリに格納しています。後半は、Figma Plugin経由でカラーを抽出できるようにします。
https://github.com/activeguild/color-extractor
(後半) Figma Plugin経由で、レポジトリからカラーを抽出してFigmaに反映する
以下のような構成で、Pluginを作成します。
Netlify Functions
を利用していますが、ここはなんでもよかったりします。
PluginがUIとPlugin Codeに分かれています。
詳細は、plugin-docs/creating-uiを参照してください。
今回は、Plugin側のコードは非常にシンプルです。
-
Github-API を利用してレポジトリのZipファイルを取得したいので、対象となるレポジトリの
レポジトリ名
OWNER名
ブランチ名
を入力できるようにします。
今回は、token
を入力としてうけとらないので、public
レポジトリのみが対象となります。
<div>
<p>
Owner:
<input
id="owner"
placeholder="username or organization name"
style="width: 200px"
/>
</p>
<p>
Repo:
<input id="repo" placeholder="repository name" style="width: 200px" />
</p>
<p>Branch: <input id="branch" value="master" style="width: 200px" /></p>
<button id="btnExtract">Extract</button>
<button id="btnCancel">Cancel</button>
</div>
...
<script>
const btnExtract = document.getElementById("btnExtract");
btnExtract.onclick = () => {
btnExtract.setAttribute("disabled", true);
const textOwner = document.getElementById("owner");
const textRepo = document.getElementById("repo");
const textBranch = document.getElementById("branch");
parent.postMessage(
{
pluginMessage: {
type: "extractor",
owner: textOwner.value,
repo: textRepo.value,
branch: textBranch.value,
},
},
"*"
);
};
...
</script>
- UIから受け取った、キー値を利用してNetlify Functionsを呼び出します。
呼び出したFunctionでは、前半に紹介しているGithubAPIからzipを取得しカラーコードをSVGのテキストに変換する関数が動いており、
結果として、SVGのテキストを取得できます。
type Message = {
type: "cancel" | "extract";
owner: string;
repo: string;
branch: string;
};
figma.ui.onmessage = async (message: Message) => {
if (message.type === "cancel") {
figma.closePlugin();
return;
}
try {
const response = await fetch(
`https://xxxxxxxx.netlify.app/.netlify/functions/extract?owner=${message.owner}&repo=${message.repo}&branch=${message.branch}`,
{ method: "GET" }
);
...
} catch (e: unknown) {
if (e instanceof Error) {
figma.notify(e.message);
}
} finally {
figma.ui.postMessage("finish");
}
};
- 取得した、SVGのテキストを
Node
に変換して、Pageに追加してPluginの動作としては完了です。
const svgText = await response.text();
const newNode = figma.createNodeFromSvg(svgText);
figma.currentPage.appendChild(newNode);
figma.currentPage.selection = [newNode];
- ここで紹介したコードは以下のレポジトリに格納しています。
イレギュラーなケースやエラーなどはあまり考慮できてないです。
https://github.com/activeguild/color-extractor-figma-plugin
Demo
機能追加1
まとめ
管理していなかったカラーコードを管理する足がかりとして、Figma Pluginを利用して
既存のカラーコードをFigmaに同期する方法を紹介しました。
差分管理やカラー以外も同期できるようになれば、現状の実装からデザインシステムの一部を作り上げることができるかもしれません。
今回は、cssファイルのみの解析にとどめていますが、sassやjsxファイルなども解析できるようになれば、
幅広く利用できるPluginになると考えています。
まだPluginを公開できていないので、反響がよさそうだったら作りこんで行こうと思います。
カラーコードの整理から解放されて、気持ちよく新年が迎えられることを祈っています。
記事内のリンクまとめ
https://docs.github.com/ja/rest/repos/contents?apiVersion=2022-11-28#download-a-repository-archive-zip
https://github.com/activeguild/color-extractor
https://github.com/activeguild/color-extractor-figma-plugin