Skip to content

Instantly share code, notes, and snippets.

@AJABON
Created December 23, 2025 05:40
Show Gist options
  • Select an option

  • Save AJABON/31f15ee98966159b537a701546112ed7 to your computer and use it in GitHub Desktop.

Select an option

Save AJABON/31f15ee98966159b537a701546112ed7 to your computer and use it in GitHub Desktop.
Illustrator:選択した1つの楕円形のアンカーポイントのハンドルを伸縮するやつ
#target illustrator
(function () {
// ===============================
// 前提チェック
// ===============================
if (app.documents.length === 0) {
alert("ドキュメントがありません");
return;
}
if (app.selection.length !== 1 || !(app.selection[0] instanceof PathItem)) {
alert("パスを1つだけ選択してください");
return;
}
var path = app.selection[0];
if (path.pathPoints.length !== 4) {
alert("楕円と判定できませんでした");
return;
}
var pts = path.pathPoints;
// ===============================
// 楕円判定(アンカー順非依存)
// ===============================
function classifyEllipsePoints(pts) {
var tol = 0.01;
var A = [];
for (var i = 0; i < 4; i++) {
A.push(pts[i].anchor);
}
var v = [], h = [];
for (var i = 0; i < 4; i++) {
for (var j = i + 1; j < 4; j++) {
if (Math.abs(A[i][0] - A[j][0]) < tol) v.push([i, j]);
if (Math.abs(A[i][1] - A[j][1]) < tol) h.push([i, j]);
}
}
if (v.length !== 1 || h.length !== 1) return null;
return { vertical: v[0], horizontal: h[0] };
}
if (!classifyEllipsePoints(pts)) {
alert("楕円と判定できませんでした");
return;
}
// ===============================
// bounds
// ===============================
var gb = path.geometricBounds; // [L, T, R, B]
var cx = (gb[0] + gb[2]) / 2;
var cy = (gb[1] + gb[3]) / 2;
var rx = (gb[2] - gb[0]) / 2;
var ry = (gb[1] - gb[3]) / 2;
// ===============================
// 初期状態バックアップ
// ===============================
var backup = [];
for (var i = 0; i < 4; i++) {
backup.push({
l: pts[i].leftDirection.slice(),
r: pts[i].rightDirection.slice()
});
}
// ===============================
// 向き保存ハンドル設定(核心)
// ===============================
function setHandlePreserveDir(p, a, len, isHorizontal) {
function s(v) {
return v === 0 ? 0 : (v > 0 ? 1 : -1);
}
var l = p.leftDirection;
var r = p.rightDirection;
var sxL = s(l[0] - a[0]);
var syL = s(l[1] - a[1]);
var sxR = s(r[0] - a[0]);
var syR = s(r[1] - a[1]);
// 両方ゼロ
if (sxL === 0 && syL === 0 && sxR === 0 && syR === 0) {
if (isHorizontal) {
sxL = -1; sxR = 1;
} else {
syL = -1; syR = 1;
}
}
// 片側ゼロ
else if (sxL === 0 && syL === 0) {
sxL = -sxR; syL = -syR;
}
else if (sxR === 0 && syR === 0) {
sxR = -sxL; syR = -syL;
}
p.leftDirection = [
a[0] + (isHorizontal ? sxL * len : 0),
a[1] + (isHorizontal ? 0 : syL * len)
];
p.rightDirection = [
a[0] + (isHorizontal ? sxR * len : 0),
a[1] + (isHorizontal ? 0 : syR * len)
];
}
// ===============================
// ハンドル適用
// ===============================
function applyHandles(vRatio, hRatio) {
var vLen = ry * vRatio;
var hLen = rx * hRatio;
for (var i = 0; i < 4; i++) {
var p = pts[i];
var a = p.anchor;
var dx = Math.abs(a[0] - cx);
var dy = Math.abs(a[1] - cy);
if (dy > dx) {
// 上下アンカー → 左右ハンドル
setEllipseHandleByIndex(p, i, hLen, true);
} else {
// 左右アンカー → 上下ハンドル
setEllipseHandleByIndex(p, i, vLen, false);
}
}
}
// ===============================
// 初期比率算出
// ===============================
function initialRatio() {
var h = 0, v = 0, hc = 0, vc = 0;
for (var i = 0; i < 4; i++) {
var a = pts[i].anchor;
var l = pts[i].leftDirection;
var r = pts[i].rightDirection;
var dx = Math.abs(a[0] - cx);
var dy = Math.abs(a[1] - cy);
if (dy > dx) {
h += (Math.abs(l[0] - a[0]) + Math.abs(r[0] - a[0])) / 2;
hc++;
} else {
v += (Math.abs(l[1] - a[1]) + Math.abs(r[1] - a[1])) / 2;
vc++;
}
}
return {
h: hc ? h / hc / rx : 0,
v: vc ? v / vc / ry : 0
};
}
var init = initialRatio();
// ===============================
// UI
// ===============================
var w = new Window("dialog", "Ellipse Handler");
w.alignChildren = "fill";
var chk = w.add("checkbox", undefined, "天地左右を連動");
chk.value = false;
function addSlider(label, val) {
var g = w.add("group");
g.add("statictext", undefined, label);
var s = g.add("slider", undefined, val * 100, 0, 100);
var t = g.add("edittext", undefined, Math.round(val * 100));
t.characters = 4;
return { s: s, t: t };
}
var hUI = addSlider("天地", init.h);
var vUI = addSlider("左右", init.v);
function sync(fromV) {
if (!chk.value) return;
if (fromV) {
vUI.s.value = hUI.s.value;
vUI.t.text = hUI.t.text;
} else {
hUI.s.value = vUI.s.value;
hUI.t.text = vUI.t.text;
}
}
function update() {
applyHandles(vUI.s.value / 100, hUI.s.value / 100);
app.redraw();
}
vUI.s.onChanging = function () {
vUI.t.text = Math.round(this.value);
sync(false);
update();
};
hUI.s.onChanging = function () {
hUI.t.text = Math.round(this.value);
sync(true);
update();
};
chk.onClick = function () {
sync(true);
update();
};
var btn = w.add("group");
btn.alignment = "right";
btn.add("button", undefined, "OK");
var cancel = btn.add("button", undefined, "Cancel");
cancel.onClick = function () {
for (var i = 0; i < 4; i++) {
pts[i].leftDirection = backup[i].l;
pts[i].rightDirection = backup[i].r;
}
w.close();
};
function setEllipseHandleByIndex(p, idx, len, isHorizontal) {
var ax = p.anchor[0];
var ay = p.anchor[1];
if (isHorizontal) {
// 上下アンカー → 左右ハンドル(天地スライダー)
if (idx === 1) { // 下
p.leftDirection = [ax + len, ay];
p.rightDirection = [ax - len, ay];
} else if (idx === 3) { // 上
p.leftDirection = [ax - len, ay];
p.rightDirection = [ax + len, ay];
}
} else {
// 左右アンカー → 上下ハンドル(左右スライダー)
if (idx === 0) { // 右
p.leftDirection = [ax, ay + len];
p.rightDirection = [ax, ay - len];
} else if (idx === 2) { // 左
p.leftDirection = [ax, ay - len];
p.rightDirection = [ax, ay + len];
}
}
}
w.show();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment