Adobe Illustrator ベジェ曲線の描画コマンドをスクリプトで取得する

## はじめに

Adobe Illustratorのオブジェクトをcreate.js用の描画コマンドに変換するスクリプトを作りましたのでご紹介します。

### Adobe Illustrator Extend Scriptとは

Adobe IllustratorはJavaScriptで操作が可能です。

[公式リファレンス](https://www.adobe.com/devnet/illustrator/scripting.html)

定型処理をスクリプト化しておくことで、作業効率を向上させることができます。
本記事のスクリプトはこの機能を利用しています。

## 機能

-   パスで描画されたオブジェクトをcreate.jsのgraphicsコマンドに変換します。
-   選択されたオブジェクトを1つのcreatejs.Shapeオブジェクトとして出力します。
-   オブジェクトの重ね順は維持されます。
-   レイヤー効果やグラデーションは再現できません。
-   リンク、埋め込みされた画像には非対応です。

## スクリプト

<details><summary>スクリプトのソースコード</summary><div>

```js
'use strict';

function getInitialSelections() {
  if (app == null) {
    return;
  }

  if (!app.activeDocument) {
    alert("対象となるドキュメントを開いてからスクリプトを実行してください");
    return;
  }

  var n = activeDocument.selection.length;

  if (n <= 0) {
    alert("座標を書き出したいオブジェクトを選択してからスクリプトを実行してください");
    return;
  }

  return activeDocument.selection;
}

/**
 * 指定された名前のレイヤーを作成する。
 * すでにその名前のレイヤーが存在する場合はそのレイヤーを返す。
 * @param {string} layerName
 * @returns {*} layer
 */
function addLayer (layerName) {
  var layer;
  var myDoc = app.activeDocument;
  var n = myDoc.layers.length;

  for (var i = 0; i < n; i++) {
    if (myDoc.layers[i].name === layerName) {
      layer = myDoc.layers[i];
      layer.locked = false;
      layer.visible = true;
      break;
    }
  }

  if (!layer) {
    layer = myDoc.layers.add();
    layer.name = layerName;
  }

  return layer;
}

/**
 * TextFrameItemの文字全てに塗りの色を指定する。
 * @param {TextFrameItem} obj
 * @param {number} r
 * @param {number} g
 * @param {number} b
 */
function setTextFrameColor (obj, r, g, b) {
  var fillColor = new RGBColor();
  fillColor.red = r;
  fillColor.green = g;
  fillColor.blue = b;

  for (var i = 0, n = obj.characters.length; i < n; i++) {
    var chr = obj.characters[i];
    chr.fillColor = fillColor;
  }
}

/**
 * 指定されたレイヤーにTextFrameItemを追加する。
 * @param layer TextFrameItemを追加するレイヤー
 * @param text TextFrameItemの内容
 * @param bounds TextFrameItemの座標 [0] = x, [1] = y
 */
function addTextFrameItem (layer, text, bounds) {
  var textFrame = layer.textFrames.add();
  textFrame.contents = text;
  textFrame.translate(bounds[0], bounds[1]);
  setTextFrameColor(textFrame, 255, 0, 255);
  return textFrame;
}

/**
 * 塗りカラー情報を取得する。
 * 書式はCSSのrgba(r,g,b,a)に準ずる。
 * @param {PathItem|TextFrameItem} obj
 */
function getFillColorAsCSS(obj) {
  switch (obj.typename) {
    case "TextFrame":
      return getTextFillColorAsCSS(obj);

    case "PathItem":
      return getPathFillColorAsCSS(obj);
  }

  return null;
}
/**
 * 線色情報を取得する。
 * 書式はCSSのrgba(r,g,b,a)に準ずる。
 * @param {PathItem|TextFrameItem} obj
 * @returns {string|null}
 */
function getStrokeColorAsCSS(obj) {
  switch (obj.typename) {
    case "TextFrame":
      return getTextStrokeColorAsCSS(obj);

    case "PathItem":
      return getPathStrokeColorAsCSS(obj);
  }

  return null;
}

/**
 * TextFrameItemの塗りからCSS文字列を取得する。
 * @private
 * @param obj
 * @returns {string}
 */
var getTextFillColorAsCSS = function getTextFillColorAsCSS(obj) {
  var chr = obj.characters[0];
  var color = chr.fillColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * TextFrameItemの塗りからCSS文字列を取得する。
 * @private
 * @param obj
 * @returns {string}
 */
var getTextStrokeColorAsCSS = function getTextStrokeColorAsCSS(obj) {
  var chr = obj.characters[0];
  var color = chr.strokeColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * PathItemの塗りからCSS文字列を取得する。
 * @param obj
 * @returns {string}
 */
var getPathFillColorAsCSS = function getPathFillColorAsCSS(obj) {
  var color = obj.fillColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * PathItemの線色からCSS文字列を取得する。
 * @param obj
 * @returns {string}
 */
var getPathStrokeColorAsCSS = function getPathStrokeColorAsCSS(obj) {
  var color = obj.strokeColor;

  if (color.typename !== "RGBColor") {
    return null;
  }

  var opacity = obj.opacity;
  return getColorAsCSS(color.red, color.green, color.blue, opacity);
};

/**
 * rgbaの数値をCSS文字列に変換する。
 * @private
 * @param r
 * @param g
 * @param b
 * @param a
 * @returns {string}
 */
var getColorAsCSS = function getColorAsCSS(r, g, b, a) {
  if (a === 100) {
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }

  return "rgba(" + r + ", " + g + ", " + b + ", " + a / 100 + ")";
};

/**
 * アートボード座標系の座標配列をルーラー原点の座標配列に変換する。
 * @param pos{Array}
 * @return {number[]}
 */
function convertToRulerOrigin(pos) {
  return [pos[0], -pos[1]];
}

/**
 * ランダムな文字列を指定された文字数で生成する。
 * @param len
 * @returns {string}
 */
function generateRandomString(len) {
  var l = len; // 生成する文字列の長さ

  var c = "abcdefghijklmnopqrstuvwxyz"; // 生成する文字列に含める文字セット

  var cl = c.length;
  var r = "";

  for (var i = 0; i < l; i++) {
    r += c[Math.floor(Math.random() * cl)];
  }

  return r;
}

/**
 * グループ / レイヤーアイテムのchildren配列を取得する。
 * 空の場合は空配列を返す。
 * @return {*[]}
 */
function getChildren(obj) {
  var children = [];
  /**
   * pageItemsは存在するか不定のプロパティなためtryブロックで処理を行う。
   * hasOwnPropertyで存在確認を行うとerrorで落ちる。
   */

  try {
    //pageItemsは配列ではなくコレクションなため、concatではなくpushでコピーを行う。
    var n = obj.pageItems.length;

    for (var i = 0; i < n; i++) {
      children.push(obj.pageItems[i]);
    }
  } catch (e) {}

  return children;
}

/**
 * ベジェ曲線をcreatejsのgraphicsコマンドとして取得する。
 *
 * ルーラー座標を原点とする場合、
 * 「表示->定規->アートボード定規に変更」
 * が設定されている必要がある。
 * https://helpx.adobe.com/jp/illustrator/using/rulers-grids-guides-crop-marks.html
 * ウィンドウ定規の設定では、ルーラーは無視される。
 *
 * 現状では、選択されたオブジェクトが全て共通のレイヤーに所属している前提で動作している。
 * 異なるレイヤーに所属するオブジェクトを選択した場合、重なり順が狂う可能性がある。
 */

var resultLayer;
var resultLayerName = "_Bezier"; //改行、区切りコード

var CR = String.fromCharCode(13); //ポイント配列のインデックス。x, yはそれぞれ0と1に格納される。

var X = 0;
var Y = 1;
main();

function main() {
  var selections = getInitialSelections();
  if (selections == null) return;
  $.writeln(activeDocument.pageItems); //app.selectionは必ず最前面から順に格納されているので、描画順のために反転させる。

  selections.reverse();
  resultLayer = addLayer(resultLayerName); //現状の座標系設定を保存、座標系をアートボード原点に変更

  var currentSystem = app.coordinateSystem;
  app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM; //新規のshapeオブジェクトを宣言

  var shapeName = "Shape_" + generateRandomString(4);
  var result = "const get" + shapeName + " = () =>{" + CR + ("  const shape = new createjs.Shape();" + CR) + ("  const g = shape.graphics;" + CR) + ("  const drawBezier = ( points ) =>{" + CR) + ("    points.forEach ( ( p, index ) =>{" + CR) + ("      if( index === 0 ) { g.mt(...p); return;}" + CR) + ("      g.bt(...p);" + CR) + ("    });" + CR) + ("  };" + CR);

  for (var i = 0; i < selections.length; i++) {
    var command = outputPath(selections[i]);
    if (command != null) result += command;
  }

  result += "  return shape;" + CR + "}" + CR;
  addTextFrameItem(resultLayer, result, getSelectionsLeftTop(selections)); //座標系を元に戻す

  app.coordinateSystem = currentSystem;
}

function outputPath(obj) {
  //グループオブジェクトの場合、子アイテムの再帰処理。
  if (obj.typename === "GroupItem") {
    var children = getChildren(obj);
    children.reverse();
    var result = "";
    var n = children.length;

    for (var i = 0; i < n; i++) {
      result += outputPath(children[i]);
    }

    return result;
  } //選択した対象がパスではないので終了


  if (obj.typename !== "PathItem") {
    return;
  }

  return getPathCommand(obj);
}

/**
 * パスのグラフィックコマンドを取得する。
 * @param obj
 */
function getPathCommand(obj) {
  var result = "";
  var fillColor = getFillColorAsCSS(obj);
  var strokeColor = getStrokeColorAsCSS(obj); //塗り線のカラーがない場合は中断。

  if (fillColor == null && strokeColor == null) {
    return result;
  } //graphicsに塗りとストロークの開始を宣言


  result += "  g";

  if (fillColor != null) {
    result += ".f(\"" + fillColor + "\")";
  }

  if (strokeColor != null) {
    result += ".s(\"" + strokeColor + "\").ss(" + obj.strokeWidth + ")";
  }

  result += ";" + CR; //各アンカーポイントを繋ぐストロークコマンド

  result += getStrokes(obj); //ストロークの終了宣言

  result += "  g";

  if (fillColor != null) {
    result += ".ef()";
  }

  if (strokeColor != null) {
    result += ".es()";
  }

  result += ";" + CR;
  return result;
}

/**
 * パスのストロークコマンドを取得する。
 * @param obj
 * @returns {string}
 */
function getStrokes(obj) {
  //.mtコマンドで始点に移動。
  var anchor = convertToRulerOrigin(obj.pathPoints[0].anchor);
  var result = "  drawBezier([" + CR;
  result += "    [" + anchor[X] + ", " + anchor[Y] + "]" + CR; //.btコマンドでベジェ曲線を描画。

  var n = obj.pathPoints.length;

  for (var i = 1; i < n; i++) {
    result += addStroke(obj, i);
  } //クローズパスの場合、最後のアンカーと始点を結ぶ。


  if (obj.closed) {
    result += addStroke(obj, 0);
  }

  result += "  ]);" + CR;
  return result;
}

/**
 * 指定されたインデックスのアンカーポイントまでのカーブ描画コマンドを生成する。
 * 0が指定された場合は、パス末尾から始点に繋がるカーブの描画コマンドを生成する。
 * @param obj
 * @param i インデックス番号
 * @return {string}
 */
function addStroke(obj, i) {
  var result = "    , [";
  var cp1Index = i - 1;

  if (i <= 0) {
    cp1Index = obj.pathPoints.length - 1;
  }

  var pos = obj.pathPoints;
  var anchor = convertToRulerOrigin(pos[i].anchor);
  var cp1 = convertToRulerOrigin(pos[cp1Index].rightDirection);
  var cp2 = convertToRulerOrigin(pos[i].leftDirection);
  result += cp1[X] + ", " + cp1[Y] + ",";
  result += cp2[X] + ", " + cp2[Y] + ",";
  result += anchor[X] + ", " + anchor[Y] + "]" + CR;
  return result;
}

/**
 * 選択対象からバウンディングボックスの左上座標を取得する。
 * @param selections
 * @return {*[]} xy座標の配列 array[0] = x , array[1] = y; y座標はイラストレーター標準のY軸が下でマイナス方向のもの
 */
function getSelectionsLeftTop(selections) {
  var currentSystem = app.coordinateSystem;
  app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM;
  var x, y;
  var n = selections.length;

  for (var i = 0; i < n; i++) {
    var target = selections[i];
    var bounds = target.geometricBounds;
    var targetX = Math.round(bounds[0]);
    var targetY = Math.round(bounds[1]);
    if (x === undefined) x = targetX;
    if (y === undefined) y = targetY;
    x = Math.min(x, targetX);
    y = Math.max(y, targetY);
  }

  if (x === undefined) x = 0;
  if (y === undefined) y = 0;
  app.coordinateSystem = currentSystem;
  return [x, y];
}
```
</div></details>

ソースコードが非常に長いため、折りたたんで掲載しています。

関数単位で分割しながら開発を行い、[rollup.js](https://rollupjs.org/guide/en/)で連結してillustratorで動作するスクリプトを出力しています。バンドルの方法に関しては、以下の記事をご参照ください。

[Babel + rollup.jsでES3環境向けトランスパイル](https://qiita.com/masato_makino/items/8a61e97ea67f101cf1ae)

それぞれの関数は切り出して使用することもできます。ぜひご利用ください。

## 出力例

illustrator上でドキュメントを開き、出力したいオブジェクトを選択します。この状態でスクリプトを実行すると、以下のようなテキストオブジェクトが出力されます。
この例はハート型のオブジェクトを出力したものです。

```js
const getShape_vsrb = () =>{
  const shape = new createjs.Shape();
  const g = shape.graphics;
  const drawBezier = ( points ) =>{
    points.forEach ( ( p, index ) =>{
      if( index === 0 ) { g.mt(...p); return;}
      g.bt(...p);
    });
  };
  g.s("#e61e2c").ss(1);
  drawBezier([
    [199.999692558295, 139.037809861326]
    , [199.999692558295, 139.037809861326,216.51750134205, 91.0703552349714,260.408864739811, 101.478048251449]
    , [304.306569058068, 111.887435228376,302.040473463536, 149.898168782486,297.969050788654, 167.089794944798]
    , [293.891253219555, 184.314831799952,266.291989537882, 213.247676318413,241.405592420213, 229.538070494225]
    , [216.51750134205, 245.828464670041,201.360366325656, 263.475297677716,199.999692558295, 269.360116287618]
    , [198.644947652663, 263.475297677716,183.481883774537, 245.828464670041,158.595486656872, 229.538070494225]
    , [133.707395578709, 213.247676318413,106.111932139902, 184.314831799952,102.030334327937, 167.089794944798]
    , [97.9589002789444, 149.898168782486,95.6966160382808, 111.887435228376,139.592214337274, 101.478048251449]
    , [183.481883774537, 91.0703552349714,199.999692558295, 139.037809861326,199.999692558295, 139.037809861326]
  ]);
  g.es();
  return shape;
}
```

出力されたアロー関数を実行するとcreatejs.Shapeのインスタンスが得られます。これをstageに配置すれば描画完了です。

#### 座標値の書式について

drawBezier関数に読み込ませている配列の書式は、[Canvas​Rendering​Context2D.moveTo()
](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/moveTo)および[Canvas​Rendering​Context2D.bezier​CurveTo()](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/bezierCurveTo)関数の引数に準じています。

また自作のモーションパスアニメーションモジュールを製作しており、座標配列はこのモジュールにそのまま読み込めます。アニメーションモジュールに関しては、以下の記事をご参照ください。

[モーションパスアニメーションのモジュールを作った](https://qiita.com/masato_makino/items/f1de1d96d9f18cd5a18b)

以上、ありがとうございました。