Created
December 23, 2025 05:40
-
-
Save AJABON/31f15ee98966159b537a701546112ed7 to your computer and use it in GitHub Desktop.
Illustrator:選択した1つの楕円形のアンカーポイントのハンドルを伸縮するやつ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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