7
7

More than 1 year has passed since last update.

HTML/CSSコーダーにもXDのプラグインの自作をお勧めする件

Last updated at Posted at 2022-03-10

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の「プラグイン → 開発版 → 開発フォルダーを表示」から開きます。(「レガシー」と出てますが、今回のようにちょっとしたものを作るなら十分です。)
Image from Gyazo

今回はdevelopフォルダ内に、このようにMyPluginフォルダ、main.jsファイル、manifest.jsonファイルを作成します。
Image from Gyazo

それぞれ以下を記述します。(一部解説を後述。)

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"
            }
        }
    ]
}
main.js
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,
    }
};

メニューへの表示を確認

「プラグイン → 開発版 → プラグインを再読込み」をクリックします。
Image from Gyazo

「プラグイン → MyPlugin」が表示されていれば成功です。
Image from Gyazo

使い方

テキスト要素を選択した状態1で「Copy Text」を実行2すると、文字をコピーできます3

グループを選択した状態1で「Copy Text」を実行2すると、グループに含まれるテキスト要素全てからコピーされます3。見た目の順番通りにはなりませんので、HTMLに貼り付けるときは適宜順番を調整しましょう。

テキスト要素を選択した状態1で「Copy Property(px)」を実行2すると、font-sizecolorなどをコピーできます3。「vw」の方4ではvw単位でコピーできます5

四角要素を選択した状態では、width, height など。

四角要素は、塗りがグラデーションであれば linear-gradient で取得できるようにしました。

線要素を選択した状態では、border-bottom として取り出します。(上記全体的にですが、CSSでは適宜修正して使うことを想定しています。この例では border-left に修正する必要がありますし、斜めの線では transform と組み合わせるか画像にするか...など工夫が必要です。)

軽く解説

XDのメニュー、manifest.json、main.jsの関係

  • manifest.jsonnameuiEntryPoints.labelがXDのメニューに表示されます。
  • manifest.jsonuiEntryPoints.commandIdmain.jsmodule.exports.commandsに対応する関数を呼び出します。

main.js(XD関連の部分のみピックアップして解説します。)

フロントのコーディングが多い人はrequireに馴染みがないかもしれませんが、サーバサイドやある種のツール上でjavascriptを動かす際にはよく出てきます。ここでは、XDのText, Line, Rectangle, ... の各要素や、クリップボードの機能を読み込んでいます。
Image from Gyazo

実行される関数の第1引数selectionには、XDで選択されている物の情報が入っています。selection.items で、選択されている要素の配列を参照できます。

instanceof は、オブジェクトの種類を判定します。ここでは、選択された要素の親要素を辿ってアートボードを探し、artBoardWidth変数にアートボードの横幅を代入しています。vwの計算に使います。

item.textで、テキスト要素の文字を取り出します。GroupSymbolInstanceの場合、再帰してグループ内のテキスト要素を探します。
Image from Gyazo

Text要素の場合、Line要素の場合、Rectangle要素の場合...と、要素の種類ごとにプロパティを取り出して、css形式に変換しています。

cssのletter-spacingが、XDオブジェクトではcharSpacingと名前が違ったり1000倍した値だったりと、形式が異なるので、カスタマイズする際はこちら↓のリファレンスを参照しましょう。

console.logでXDのコンソールに出力できます。デバッグに使います。XDのコンソールは、XDの「プラグイン → 開発版 → 開発者コンソール」から開きます。
Image from Gyazo

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7