Skip to content

Instantly share code, notes, and snippets.

@S4K4K0
Created June 10, 2021 07:53
Show Gist options
  • Save S4K4K0/37d93d1425896af769bf7f11601b42ab to your computer and use it in GitHub Desktop.
Save S4K4K0/37d93d1425896af769bf7f11601b42ab to your computer and use it in GitHub Desktop.
// plotCurvature.jsx Copyright (c) 2021 koji sakai
// 選択したパス上に曲率コームを描画するIllustrator用のExtendScriptです
//
// * 使いかた *
// パスを選択しスクリプトを実行します。パスのもつ曲率によって表示される大きさに差がでますので、拡大率をスライダーで調整してください。
// 選択されたパスの数によってはものすごく重くなりますのでプレビューはデフォルトオフにしています。main()冒頭の conf オブジェクトで設定変更できます。
// 曲率を表示する点の分割数(法線の密度)や、描画色などについても、conf オブジェクト内のパラメータにてお好みに調整してください。
// このスクリプトを用いた結果として生じたいかなる損害・不利益について責任は持てません(いちおう)。
//
// ---------------------------------------------
// 2021/06/10 公開
//
// ---------------------------------------------
// 以下、利用させていただいているサンプル、関数については
// Copyright(c) 2014-2016 Hiroyuki Sato
//
// プレビュー部分やUIは以下のサンプルを参考に改変させて頂きました
// https://qiita.com/shspage/items/441ccf61394d9c504beb
//
// parseIdx() 関数は以下のコードを利用させていただいています
// https://github.com/shspage/illustrator-scripts/blob/master/breakDashes.jsx
// ---------------------------------------------
//
// This script is distributed under the MIT License.
// See the LICENSE file for details.
main();
function main() {
// 設定リストです。
// ダイアログの初期値の設定や、外部処理に値をまとめて渡すために使用しています。
var conf = {
rate: "100", // 曲率表示倍率の初期値
maxSliderValue: 1000, // 拡大率スライダー最大値
cvtColor: [100, 0, 0, 0], // [c,m,y,k] (0 to 100)
previewed: false, // プレビューのデフォルト値
step: 80 // 単一Bezierセグメント内の法線数
}
// 選択範囲からパスだけを取得します。
var paths = extractPathsInSelection();
// パスがない場合は終了です。
if (paths.length < 1) {
alert("no path in the selection");
return;
}
// 対象とするパスがなくても終了
if (!isValid(paths)) {
alert("no valid path");
return;
}
var clearPreview = function() {
if (conf.previewed) {
// ↑プレビューフラグが立っているとき(プレビューされているとき)
// のみ実行されます。
try {
undo();
// 次の redraw をしないとクラッシュする場合があります。
// 大丈夫な場合も多いのですが。理由はまだよくわかっていません。
redraw();
} catch (e) {
// 万一エラーが発生した場合は警告を表示します。
alert(e);
} finally {
// プレビュー中フラグをリセットします。
conf.previewed = false;
}
}
}
var drawPreview = function() {
// プレビューを描画します。外部のメソッドに処理を回します。
if (conf.rate > 1.0) {
try {
// try でくくっていても、メイン処理中でエラーになると
// ダイアログがフリーズ状態になることがありますので、
// 外部のメソッドは事前に単独で十分テストしてください。
plotCurvature(conf, paths);
} finally {
// プレビュー中フラグを立てます。
conf.previewed = true;
}
}
}
// ダイアログの表示 ----------------------------
var win = new Window("dialog", "Plot Curvature Comb");
win.alignChildren = "fill";
// スライダー
win.rateSliderPanel = win.add("panel", undefined, "magnification");
win.rateSliderPanel.orientation = "row";
win.rateSliderPanel.alignChildren = "fill";
win.rateSliderPanel.rateSlider = win.rateSliderPanel.add(
"slider", undefined, conf.rate, 10, conf.maxSliderValue);
win.rateSliderPanel.rateSlider.size = "width: 200, height: 30";
win.rateSliderPanel.valueText = win.rateSliderPanel.add(
"statictext", undefined, conf.rate);
win.rateSliderPanel.valueText.characters = 6;
// チェックボックス
win.chkGroup = win.add("group");
win.chkGroup.alignment = "center";
win.chkGroup.previewChk = win.chkGroup.add("checkbox", undefined, "preview");
win.chkGroup.previewChk.value = false;
// ボタン
win.btnGroup = win.add("group", undefined);
win.btnGroup.alignment = "center";
win.btnGroup.okBtn = win.btnGroup.add("button", undefined, "OK");
win.btnGroup.cancelBtn = win.btnGroup.add("button", undefined, "Cancel");
var getValues = function() {
// 入力値を設定リストに割り当てます。
conf.rate = win.rateSliderPanel.valueText.text;
}
var processPreview = function(is_preview) {
// プレビュー処理をします。
// 引数の is_preview は、プレビューのとき true
// OK ボタンを押して処理を確定するとき false です。
if (!is_preview || win.chkGroup.previewChk.value) {
try {
// プレビュー中にダイアログの操作をされないように、enabled を設定します。
win.enabled = false;
// 入力値を取得します。
getValues();
// プレビュー中の場合はいったん元に戻します。
clearPreview();
// プレビューの描画を行います。
drawPreview();
// プレビューの場合は、画面を強制的に再描画します。
if (is_preview) redraw();
} catch (e) {
alert(e);
} finally {
win.enabled = true;
}
}
}
win.rateSliderPanel.rateSlider.onChanging = function() {
// スライダーを動かしているときの処理。
// 実際の処理を行うと重くなるため、スライダー脇の値のみ変更しています。
win.rateSliderPanel.valueText.text = this.value.toFixed(0);
}
win.rateSliderPanel.rateSlider.onChange = function() {
// スライダーを動かし終えたときの処理。プレビュー処理を行います。
win.rateSliderPanel.valueText.text = this.value.toFixed(0);
processPreview(true);
}
win.chkGroup.previewChk.onClick = function() {
// プレビューチェックボックスをクリックしたときの処理。
if (this.value == true) {
// チェックを入れた場合。
processPreview(true);
} else {
// チェックを外した場合。プレビュー中の場合は元に戻します。
if (conf.previewed) {
clearPreview();
redraw();
}
}
}
win.btnGroup.okBtn.onClick = function() {
// OK ボタンをクリックしたときの処理。
// プレビュー中の場合は、何もせず終了。
// そうでない場合はプレビュー描画時と同じ処理をしてから終了します。
if (!conf.previewed) processPreview(false);
win.close();
}
win.btnGroup.cancelBtn.onClick = function() {
// キャンセルボタンをクリックした時の処理。
// プレビュー中の場合は元に戻します。
try {
win.enabled = false;
clearPreview();
} catch (e) {
alert(e);
} finally {
win.enabled = true;
}
win.close();
}
// ダイアログを表示します。
win.show();
}
// --------------------------------------
// メインの処理です。
function plotCurvature(conf, paths) {
var strokeCol = new CMYKColor();
strokeCol.cyan = conf.cvtColor[0];
strokeCol.magenta = conf.cvtColor[1];
strokeCol.yellow = conf.cvtColor[2];
strokeCol.black = conf.cvtColor[3];
for (var i = 0; i < paths.length; i++) {
var pps = paths[i].pathPoints;
var grpItm = activeDocument.activeLayer.groupItems.add();
for (var j = 0; j < pps.length; j++) {
nxi = parseIdx(pps, j + 1);
if (nxi < 0) break;
var q = [
pps[j].anchor,
pps[j].rightDirection,
pps[nxi].leftDirection,
pps[nxi].anchor
];
for (var k = 0; k <= conf.step; k++) {
var t = k / conf.step;
var bzvec = bezierPnt(q, t);
var bznrm = bezNormal(q, t);
var cvt = bezCurvature(q, t);
if (Math.abs(cvt) >= 0.0000001) {
bznrm[0] = -bznrm[0] * cvt * conf.rate;
bznrm[1] = -bznrm[1] * cvt * conf.rate;
var pObj = grpItm.pathItems.add();
pObj.setEntirePath([
[bzvec[0], bzvec[1]],
[bzvec[0] + bznrm[0], bzvec[1] + bznrm[1]]
]);
pObj.strokeWidth = 0.5;
pObj.filled = false;
pObj.strokeColor = strokeCol;
}
}
}
}
}
// --------------------------------------
// 選択範囲からパスのみを配列に入れて返します。
function extractPathsInSelection(sels, paths) {
if (!sels) sels = app.activeDocument.selection;
if (!paths) paths = [];
for (var i = 0; i < sels.length; i++) {
if (sels[i].typename == "PathItem") {
paths.push(sels[i]);
} else if (sels[i].typename == "GroupItem") {
extractPathsInSelection(sels[i].pageItems, paths);
} else if (sels[i].typename == "CompoundPathItem") {
extractPathsInSelection(sels[i].pathItems, paths);
}
}
return paths;
}
// --------------------------------------
// 孤立点でないパスが一つ以上あることを確認
function isValid(paths) {
var result = false;
for (var i = 0; i < paths.length; i++) {
if (paths[i].pathPoints.length > 1) result = true;
}
return result;
}
// --------------------------------------
// ヘルパー関数
function culcCoef(q) {
var coef = {
ax: -q[0][0] + 3 * q[1][0] - 3 * q[2][0] + q[3][0],
ay: -q[0][1] + 3 * q[1][1] - 3 * q[2][1] + q[3][1],
bx: 3 * q[0][0] - 6 * q[1][0] + 3 * q[2][0],
by: 3 * q[0][1] - 6 * q[1][1] + 3 * q[2][1],
cx: -3 * q[0][0] + 3 * q[1][0],
cy: -3 * q[0][1] + 3 * q[1][1]
}
return coef;
}
function bezierPnt(q, t) {
var u = 1 - t;
return [u * u * u * q[0][0] + 3 * u * t * (u * q[1][0] + t * q[2][0]) + t * t * t * q[3][0],
u * u * u * q[0][1] + 3 * u * t * (u * q[1][1] + t * q[2][1]) + t * t * t * q[3][1]
];
}
function bezTangent(q, t) {
var coef = culcCoef(q);
with(coef) {
var x = 3 * ax * t * t + 2 * bx * t + cx;
var y = 3 * ay * t * t + 2 * by * t + cy;
}
return [x, y];
}
function bezNormal(q, t) {
var tan = bezTangent(q, t);
tan = normalizeVec(tan);
return [-tan[1], tan[0]];
}
function bezCurvature(q, t) {
var coef = culcCoef(q);
with(coef) {
var d1x = 3 * ax * t * t + 2 * bx * t + cx;
var d1y = 3 * ay * t * t + 2 * by * t + cy;
var d2x = 6 * ax * t + 2 * bx;
var d2y = 6 * ay * t + 2 * by;
}
var divisor = (d1x * d1x + d1y * d1y) * Math.sqrt(d1x * d1x + d1y * d1y);
var cvt = divisor == 0 ?
0 :
(d1x * d2y - d2x * d1y) / divisor;
return cvt;
}
function normalizeVec(v) {
var norm = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
return [v[0] / norm, v[1] / norm];
}
// -----------------------------------------------
// return pathpoint's index. when the argument is out of bounds,
// fixes it if the path is closed (ex. next of last index is 0),
// or return -1 if the path is not closed.
function parseIdx(p, n) { // PathPoints, number for index
var len = p.length;
if (p.parent.closed) {
return n >= 0 ? n % len : len - Math.abs(n % len);
} else {
return (n < 0 || n > len - 1) ? -1 : n;
}
}
// おわり
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment