Skip to content

Instantly share code, notes, and snippets.

@shspage
Last active August 7, 2022 19:35
Show Gist options
  • Select an option

  • Save shspage/6f7eca8b6f07bd2ed4af662c7fa2856a to your computer and use it in GitHub Desktop.

Select an option

Save shspage/6f7eca8b6f07bd2ed4af662c7fa2856a to your computer and use it in GitHub Desktop.
an example of Schwarz-Christoffel mapping with Adobe Illustrator and python

Adobe Illustrator と python を使った Schwarz-Christoffel mapping の一例

Schwarz-Christoffel mapping という変換手法を使って、Illustrator でこんな図を作成してみました。

square_to_circle

以下の頁を参考にさせて頂きました。
http://squircular.blogspot.jp/2015/09/schwarz-christoffel-mapping.html
ただし、大きさや向きを変更するための計算は省いています。
計算方法などの詳しい説明は、上記頁からリンクしている PDF ファイルにあります。
中心になる式だけを書くと、変換後の座標を得る式はそれぞれ以下のようになります。

四角から円に変換: F(acos(z), 1/sqrt(2))
円から四角に変換: cn(z, 1/sqrt(2))

 (ただし、z : 複素数(複素平面上の座標値)/ F : 第1種不完全楕円積分 / cn : ヤコビ楕円関数)

なるほどわからないのですが python のライブラリに頼ることにします。
Illustrator からはファイルを介して座標データをやりとりします。  

手順  

  1. Illustratorから 座標を出力
  2. それをpythonで変換して出力
  3. それをIllustratorで描画

(以下で言及しているスクリプト(DLリンクのないもの)は、下の方に掲載しています。)


1. Illustratorから 座標を出力

元になる形を Illustrator で描き、座標データをファイルに書き出します。

1-1. 曲線を折れ線化する

python側では座標値を変換するだけで曲線は扱えないので、あらかじめ曲線は折れ線化しておきます。
ここでは以前書いたスクリプト( brokenCurve.jsx )を使いました。パスを選択して実行し、元のパスとの最大誤差(単位:point)を指定して分割させます。

1-2. データを出力する

書き出す範囲を選択して、出力用スクリプト(getCoords.jsx)を実行します。

前述の PDF ファイル(p.19)にあるように、入力座標値は既定の領域の中にある必要があります。
入力が円の場合:原点を中心とした半径 1 の円。
入力が四角の場合:対角線が 2 * Ke (Ke=約1.854) の正方形を45度回転して、右の角を原点に置いた形。

area

スクリプトでは、取得した座標値をこの範囲に収まるように換算して出力します。
換算が正しく行われるように、選択範囲を囲む円周の上下左右の端、または正方形の各頂点にアンカーポイントが存在するようにしてください。

2. python で座標値を変換する

mapping.py を実行します。
やっていることは、ほぼ mpmath の関数に座標値を渡しているだけです。

3. 変換後の座標値を Illustrator で描画する

Illustrator で描画用スクリプト(drawCoords.jsx)を実行します。


補足

1.

四角の頂点近くに位置していた円は、円でなくてカージオイドっぽい形になります。こういうものなのか、誤差があるのか。 detail

2.

円 → 四角の変換結果です。円を円に写すわけではないのですね? こういうものなのか、何か間違っているのか。 circle_to_square

// Adobe Illustrator Script
// ファイルから座標値を取得して描画する。
// 入力ファイルの書式は、1行ごとに
// x座標値 y座標値
// の形式。(半角空白区切り)
// 線で描画する場合、空行をパス1つごとの区切り目とする。
// 座標値に対する倍率
var SCALE_FACTOR = 1.0;
// 線幅 (point)
var STROKE_WIDTH = 0.5;
// 座標を線として繋がずに点として描画する
var DRAW_DOTS = false;
// 座標を点(丸)として出力する場合の半径
var RADIUS = 1;
function main(){
var txt = File.openDialog("select a TXT file", "TXT files:*.txt");
if(txt && txt.open("r")){
lines = txt.read().split("\n");
txt.close();
var pitems = activeDocument.pathItems;
var p = null; // PathItem
var pts = null; // PathPoints
var dotColor = getBlack();
var err = false;
for(var i = 0; i < lines.length; i++){
if(lines[i] == ""){
// パス1つの描画の終了(線を描画するとき)
// 点で描画するときは何も行わないだけ
p = null;
continue;
}
if(DRAW_DOTS){
// 点をドットで描画
err = drawPoint(pitems, lines[i].split(" "), dotColor);
if(err) break;
} else {
// 線を描画
if(p == null){
// パス1つの描画の開始
p = pitems.add();
p.filled = false;
p.stroked = true;
p.strokeWidth = STROKE_WIDTH;
p.closed = true;
pts = p.pathPoints;
}
with(pts.add()){
var r = lines[i].split(" ");
anchor = [r[0] * SCALE_FACTOR,
r[1] * SCALE_FACTOR];
leftDirection = anchor;
rightDirection = anchor;
}
}
}
}
}
// 座標を点(小さい円)として描画する
// p: document.pathItems
// r: [x, y]
// dotColor: ドットの色
// 戻り値: エラー時=true, 正常時=false
function drawPoint(p, r, dotColor){
try{
// top, left, width, height
var dot = p.ellipse(r[0] *SCALE_FACTOR + RADIUS,
r[1] *SCALE_FACTOR - RADIUS,
RADIUS*2, RADIUS*2);
dot.filled = true;
dot.stroked = false;
dot.fillColor = dotColor;
} catch(e) {
alert("r=" + r + "\r" + e);
return true;
}
return false;
}
function getBlack(){
var gray = new GrayColor();
gray.gray = 100.0;
return gray;
}
main();
// Adobe Illustrator Script
// 選択パスのアンカーポイントの座標値を、選択パスを含む範囲が
// -1 <= x <= 1, -1 <= y <= 1 の範囲になるように換算して
// ファイルに出力する。
// Schwarz-Christoffel mapping の入力データ用で外周が正方形の場合 true
var MODE_SQUARE = true;
// MODE_SQUARE 用の定数
var Ke = 1.8540746773013719;
// 出力座標値は 0 <= x <= -2*Ke, -Ke < y < Ke の範囲になる
function main(){
if(app.documents.length < 1) return;
var sel = app.activeDocument.selection;
if(sel.length < 1) return;
alert("getCoords\rMODE_SQUARE=" + MODE_SQUARE.toString());
var points = [];
for(var i = 0; i < sel.length; i++){
// グループ、複合パス、効果が適用されているオブジェクトは対象外
if(sel[i].typename == "PathItem"){
getCoords(sel[i], points);
}
}
if(points.length < 1){
alert("選択オブジェクトから座標を取得できませんでした。");
return;
}
var rect = getRectSpec(points);
var k = MODE_SQUARE ? Ke : 1.0;
var rate = k * 2.0 / Math.max(rect.height, rect.width);
if(MODE_SQUARE) rect.center[0] = rect.right;
// output
var output_lines = [];
for(var i = 0; i < points.length; i++){
var p = points[i];
if(p.length < 1){
// 空の配列はパスごとの区切り。空行として出力
output_lines.push("\n");
} else {
output_lines.push(
(rate * (p[0] - rect.center[0])).toFixed(8)
+ " "
+ (rate * (p[1] - rect.center[1])).toFixed(8) + "\n");
}
}
writeToFile(output_lines.join(""), "txt");
}
// ----------------------------------------------
// dat: 出力用データ。直接 File.write される
// ext: ファイル選択ダイアログのフィルタ用拡張子。例:"txt"
function writeToFile(dat, ext){
var afile = File.saveDialog("save to a file",ext + " File:*." + ext);
if(!afile){ // cancel
return false;
}
if(!afile.open("w")){
alert("failed to open a file");
return false;
}
afile.write(dat);
afile.close();
alert("saved");
return true;
}
// ----------------------------------------------
// 座標値のリストを元に、全体を囲む矩形範囲の情報を取得する
// points: 座標値 [x, y] の配列
// 戻り値: rect { left, right, top, bottom, center:[x, y], width, height }
function getRectSpec(points){
var x = points[0][0];
var y = points[0][1];
var rect = {left:x, right:x, top:y, bottom:y, center:[x,y],
width:0, height:0};
for(var i = 1; i < points.length; i++){
var p = points[i];
if(p.length < 1) continue; // パスごとの区切り
x = p[0];
y = p[1];
if(rect.left > x) rect.left = x;
if(rect.right < x) rect.right = x;
if(rect.top < y) rect.top = y;
if(rect.bottom > y) rect.bottom = y;
}
rect.center = [(rect.left + rect.right)/2, (rect.top + rect.bottom)/2];
rect.width = rect.right - rect.left;
rect.height = rect.top - rect.bottom;
return rect;
}
// ----------------------------------------------
// item: PathItem
// points: Array
function getCoords(item, points){
var pts = item.pathPoints;
for(var i = 0; i < pts.length; i++){
points.push(pts[i].anchor);
}
points.push([]); // パスごとの区切り。空行として出力
}
main()
#!python
# coding:utf-8
from __future__ import print_function
from tqdm import tqdm # プログレスバー用
from mpmath import ellipf, ellipfun, acos
# Schwarz-Christoffel mapping
# 以下の頁を参考にさせて頂きました。ただし、変換元と大きさや向きを合わせるための計算は省いています。
# http://squircular.blogspot.jp/2015/09/schwarz-christoffel-mapping.html
# 別途作成した入力座標データを元にします。
# 入力ファイル名
INPUT_FILE = "coords.txt"
# 各行が「x座標 y座標」の形式。(ex.0.1234 -0.5678)
# 途中の空行は空行として出力される。
# mapping のための入力座標の範囲など詳しくは、上記頁からリンクしているPDF参照。
# 出力ファイル名
OUTPUT_FILE = "out_coords.txt"
# 入力ファイルと同じ形式。
# 出力時の倍率
OUTPUT_SCALE_FACTOR = 100.0
# 正方形 → 円 の変換をする場合、True。逆の場合は False
MODE_SQUARE_TO_CIRCLE = True
def loadData():
u"""ファイルから座標データを読み取る"""
coords = [];
with open(INPUT_FILE, "r") as f:
for line in f:
line = line.strip()
if line == "":
# 空行はNoneとする
coords.append(None)
continue
r = map(float, line.split(" "))
w = complex(r[0], r[1])
coords.append(w)
print("loaded")
return coords
def main():
print("OUTPUT_SCALE_FACTOR = %f" % OUTPUT_SCALE_FACTOR)
print("MODE_SQUARE_TO_CIRCLE = %s" % str(MODE_SQUARE_TO_CIRCLE))
coords = loadData()
zs = [] # 出力する複素数のリスト
m = 0.5
for w in tqdm(coords):
if w is None:
zs.append(None)
elif MODE_SQUARE_TO_CIRCLE:
# 正方形 --> 円
zs.append(ellipfun("cn", w, m))
else:
# 円 --> 正方形
zs.append(ellipf(acos(w), m))
# 出力
with open(OUTPUT_FILE, "w") as f:
for z in zs:
if z is None:
# 入力ファイルの空行は出力時も空行とする
f.write("\n")
else:
# Illustratorに描かせるので、小数点以下は4桁程度にしてみた
f.write("%.4f %.4f\n"
% (z.real * OUTPUT_SCALE_FACTOR,
z.imag * OUTPUT_SCALE_FACTOR))
print("done")
if __name__=="__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment