XDのプラグインといえばデザイナー向けの便利機能が多いと思いますが、HTML/CSSコーダーがXDから情報を取り出すときにもプラグインが便利です。一般に公開されているものもたくさんありますが、ここではXDのプラグインを自作する方法について説明します。
機能としては以下のようなものを作ります。
- グループ内のテキスト要素から文字をまとめてコピーする
- 1つのテキスト要素や四角要素から色情報などを取り出し、css形式でコピーする
また、自作することにより、以下のようなメリットがあります。
- 自分のコーディングルールに合うようにカスタマイズできる
- ちょっとjavascriptの練習になる
対象の読者
- XDのデザインカンプをもらってHTML/CSSをコーディングする人
- javascriptの基本的な文法が分かる人
XDが初めての方はこちら↓
今回も、こちら↓のサイトから、ポートフォリオのXDファイルをお借りして説明します。
プラグイン作成手順
フォルダ構成
XDでプラグインを自作する際、「開発フォルダー」にこのように配置します。
developフォルダ
└ (プラグイン名)フォルダ
└ main.js
└ manifest.json
-
main.js
: 処理の内容を記述します。 -
manifest.json
: プラグインの概要を記述します。XDのメニューに表示される文字列や、ショートカットキーの設定など。
develop
フォルダは、XDの「プラグイン → 開発版 → 開発フォルダーを表示」から開きます。(「レガシー」と出てますが、今回のようにちょっとしたものを作るなら十分です。)
今回はdevelop
フォルダ内に、このようにMyPlugin
フォルダ、main.js
ファイル、manifest.json
ファイルを作成します。
それぞれ以下を記述します。(一部解説を後述。)
{
"name": "MyPlugin",
"id": "copy-text-property",
"version": "1.0.0",
"description": "copy text from Text element, or copy property from Text, Rectangle element and so on.",
"host": {
"app": "XD",
"minVersion": "13.0.0"
},
"uiEntryPoints": [
{
"type": "menu",
"label": "Copy Text",
"commandId": "copy-text",
"shortcut": {
"mac": "Cmd+Alt+C",
"win": "Ctrl+Alt+C"
}
},
{
"type": "menu",
"label": "Copy Property(px)",
"commandId": "copy-property-px",
"shortcut": {
"mac": "Cmd+Alt+A",
"win": "Ctrl+Alt+A"
}
},
{
"type": "menu",
"label": "Copy Property(vw)",
"commandId": "copy-property-vw",
"shortcut": {
"mac": "Cmd+Alt+Shift+A",
"win": "Ctrl+Alt+Shift+A"
}
},
{
"type": "menu",
"label": "Copy Property(calc vw)",
"commandId": "copy-property-calc-vw",
"shortcut": {
"mac": "Cmd+Alt+Shift+X",
"win": "Ctrl+Alt+Shift+X"
}
}
]
}
const { Text, Line, Rectangle, Ellipse, Artboard, Group, SymbolInstance } = require("scenegraph");
const clipboard = require("clipboard");
function copyText(selection) {
copy(getLines(selection.items).join("\n"));
}
function copyPropertyPx(selection) {
copyProperty(selection.items[0], "px");
}
function copyPropertyVw(selection) {
copyProperty(selection.items[0], "vw");
}
function copyPropertyCalcVw(selection) {
copyProperty(selection.items[0], "calcvw");
}
function copyProperty(item, md) {
let ret=[];
let p = item;
while(!(p instanceof Artboard)) { p = p.parent; }
let artBoardWidth = p.width;
let f = (v,d) => parseFloat(v.toFixed(d));
let unitNum = (v) => {
if(md == "vw"){
return `${f(v/artBoardWidth*100,2)}vw`;
}
if(md == "calcvw"){
return `calc(${v} / ${artBoardWidth} * 100vw)`;
}
return f(v,2) + "px";
}
let getFontStyleByFontFamily = f => f.replace(/.*-/,"");
console.log(item);
if(item instanceof Text) {
item.styleRanges.forEach(s=>{
ret.push(`font-size: ${unitNum(s.fontSize)};`);
ret.push(`font-weight: ${weightNameToNumber(s.fontStyle || getFontStyleByFontFamily(s.fontFamily))};`);
});
ret.push(`line-height: ${f(item.lineSpacing/item.fontSize,4)}em;`);
ret.push(`color: ${toHex(item.fill.value)};`);
ret.push(`letter-spacing: ${f(item.charSpacing/1000,4)}em;`);
}
if(item instanceof Line) {
if(item.strokeEnabled && item.stroke) {
ret.push(`border-bottom: ${unitNum(item.strokeWidth)} solid ${toHex(item.stroke.value)};`);
}
}
if(item instanceof Rectangle) {
ret.push(`width: ${unitNum(item.width)};`);
ret.push(`height: ${unitNum(item.height)};`);
if(item.cornerRadii.topLeft || item.cornerRadii.topRight || item.cornerRadii.bottomRight || item.cornerRadii.bottomLeft){
if(item.cornerRadii.topLeft == item.cornerRadii.topRight &&
item.cornerRadii.topLeft == item.cornerRadii.bottomRight &&
item.cornerRadii.topLeft == item.cornerRadii.bottomLeft){
ret.push(`border-radius: ${unitNum(item.cornerRadii.topLeft)};`);
} else {
ret.push(`border-radius: ${unitNum(item.cornerRadii.topLeft)} ${unitNum(item.cornerRadii.topRight)} ${unitNum(item.cornerRadii.bottomRight)} ${unitNum(item.cornerRadii.bottomLeft)};`);
}
}
}
if(item instanceof Ellipse) {
ret.push(`width: ${unitNum(item.radiusX*2)};`);
ret.push(`height: ${unitNum(item.radiusY*2)};`);
ret.push(`border-radius: 50%;`);
}
if(item instanceof Rectangle || item instanceof Ellipse) {
if(item.fillEnabled && item.fill) {
if(item.fill.value) {
ret.push(`background-color: ${toHex(item.fill.value)};`);
} else if(item.fill.colorStops){
let c = item.fill.colorStops.map(cs=>toHex(cs.color.value)).join(",");
ret.push(`background: linear-gradient(${c});`);
}
}
if(item.strokeEnabled && item.stroke) {
ret.push(`border: ${unitNum(item.strokeWidth)} solid ${toHex(item.stroke.value)};`);
}
}
if(item.shadow && item.shadow.visible) {
ret.push(`text-shadow: ${unitNum(item.shadow.x)} ${unitNum(item.shadow.y)} ${unitNum(item.shadow.blur)} ${toHex(item.shadow.color.value)};`);
}
copy(ret.join("\n"));
}
function copy(s){
s = s.trim();
if(s==""){
return;
}
console.log(s);
clipboard.copyText(s);
}
function toHex(v) {
let c = "00000000" + v.toString(16).toUpperCase();
c = c.replace(/^.*?(..)(......)$/,"$2$1");
c = c.replace(/FF$/,"");
return '#' + c;
}
function weightNameToNumber(name) {
name = name.trim().toLowerCase().replace(" ","");
if(/^(w0|w1|thin|hairline)$/ .test(name)) { return 100; }
if(/^(w2|extralight|ultralight)$/ .test(name)) { return 200; }
if(/^(w3|light)$/ .test(name)) { return 300; }
if(/^(w4|normal|regular)$/ .test(name)) { return 400; }
if(/^(w5|medium)$/ .test(name)) { return 500; }
if(/^(w6|semibold|demibold)$/ .test(name)) { return 600; }
if(/^(w7|bold)$/ .test(name)) { return 700; }
if(/^(w8|extrabold|ultrabold)$/ .test(name)) { return 800; }
if(/^(w9|black|heavy)$/ .test(name)) { return 900; }
if(/^(extrablack|ultrablack)$/ .test(name)) { return 950; }
return 400;
}
function getLines(items) {
const lines = items.filter(i => i instanceof Text).map(i => i.text);
items.filter(item => item instanceof Group || item instanceof SymbolInstance).forEach((item)=>{
lines.push(...getLines(item.children));
});
return lines;
}
module.exports = {
commands: {
"copy-text": copyText,
"copy-property-px": copyPropertyPx,
"copy-property-vw": copyPropertyVw,
"copy-property-calc-vw": copyPropertyCalcVw,
}
};
メニューへの表示を確認
「プラグイン → 開発版 → プラグインを再読込み」をクリックします。
「プラグイン → MyPlugin」が表示されていれば成功です。
使い方
テキスト要素を選択した状態1
で「Copy Text」を実行2
すると、文字をコピーできます3
。
グループを選択した状態1
で「Copy Text」を実行2
すると、グループに含まれるテキスト要素全てからコピーされます3
。見た目の順番通りにはなりませんので、HTMLに貼り付けるときは適宜順番を調整しましょう。
テキスト要素を選択した状態1
で「Copy Property(px)」を実行2
すると、font-size
やcolor
などをコピーできます3
。「vw」の方4
ではvw単位でコピーできます5
。
四角要素を選択した状態では、width
, height
など。
四角要素は、塗りがグラデーションであれば linear-gradient
で取得できるようにしました。
線要素を選択した状態では、border-bottom
として取り出します。(上記全体的にですが、CSSでは適宜修正して使うことを想定しています。この例では border-left に修正する必要がありますし、斜めの線では transform と組み合わせるか画像にするか...など工夫が必要です。)
軽く解説
XDのメニュー、manifest.json、main.jsの関係
-
manifest.json
のname
とuiEntryPoints.label
がXDのメニューに表示されます。 -
manifest.json
のuiEntryPoints.commandId
がmain.js
のmodule.exports.commands
に対応する関数を呼び出します。
main.js(XD関連の部分のみピックアップして解説します。)
フロントのコーディングが多い人はrequire
に馴染みがないかもしれませんが、サーバサイドやある種のツール上でjavascriptを動かす際にはよく出てきます。ここでは、XDのText
, Line
, Rectangle
, ... の各要素や、クリップボードの機能を読み込んでいます。
実行される関数の第1引数selection
には、XDで選択されている物の情報が入っています。selection.items
で、選択されている要素の配列を参照できます。
instanceof
は、オブジェクトの種類を判定します。ここでは、選択された要素の親要素を辿ってアートボードを探し、artBoardWidth
変数にアートボードの横幅を代入しています。vwの計算に使います。
item.text
で、テキスト要素の文字を取り出します。Group
かSymbolInstance
の場合、再帰してグループ内のテキスト要素を探します。
Text
要素の場合、Line
要素の場合、Rectangle
要素の場合...と、要素の種類ごとにプロパティを取り出して、css形式に変換しています。
cssのletter-spacing
が、XDオブジェクトではcharSpacing
と名前が違ったり1000倍した値だったりと、形式が異なるので、カスタマイズする際はこちら↓のリファレンスを参照しましょう。
console.log
でXDのコンソールに出力できます。デバッグに使います。XDのコンソールは、XDの「プラグイン → 開発版 → 開発者コンソール」から開きます。