見出し画像

Processing でグラフを描く⑧ スピログラフ

Processing でグラフを描く 第8回目です。
子供のころ、わたしはよく家で遊んでいたんですが、スピログラフという定規が好きでした。円形の定規で外側にギザギザ(ギアの歯)がついています。それを外枠に沿わせてグルグル回しながら描くと、ステキな幾何学模様が出てくるんですね。色を変えたり、定規のサイズを入れ替えて重ねて描いたり、出てくる模様が変わっていくのが楽しかった。

ネットで検索してみたら、最近のおもちゃはすごいですね。定規が楕円や長方形のやつとか、外側、内側にギザギザがついているやつとか、パーツが進化していました。面白そうです。またやってみたくなりました。

そこで今回は、Processing でスピログラフを描いてみます。

スピログラフの数学

spirograph plot point

スピログラフを描くため、初めに少し計算をしなければなりません。図を見ながら詳しく説明します。図の角度 $${\theta}$$ に対して、プロット点である (x, y) を求めます。
大円の半径を r、小円の半径を $${ar ( a > 0)}$$ とします。スピログラフは小円の中にプロット点がありますから、プロット点の半径を $${abr ( a > 0, b > 0)}$$ とします。
角度の計算が少し面倒です。原点に対して小円の中心のなす角度を $${\theta}$$ とすると、

$$
r\theta = ar(\theta + \alpha) より\\
\alpha = \frac{(1-a)}{a}\theta
$$

したがって求めるプロット点の座標は

$$
x = r((1-a)cos{\theta} + abcos({-\alpha}))\\
y = r((1-a)sin{\theta} + absin({-\alpha}))
$$

が得られます。準備が整いました。作画していきましょう。

初めてのスピログラフ

BACKGROUND_COLOR = color(0, 44, 77)
colors = [
    [255, 255, 255],  # white
    [255, 127, 0],  # orange
    [255, 0, 255],  # magenta
    [127, 127, 255],  # lightblue
    [255, 255, 0],  # yellow
    [0, 255, 0],  # lime
    [255, 127, 127],  # pink
    [127, 127, 127],  # gray
    [191, 191, 191],  # lightgray
    [0, 255, 255],  # cyan
    [127, 0, 127],  # purple
    [0, 0, 255],  # blue
    [127, 0, 0],  # brown
    [0, 127, 0],  # green
    [255, 0, 0],  # red
    [0, 0, 0],  # black
]
SIZE = 1000
STEP = 50
RADIUS = 450
points_list = []


def setup():
    size(SIZE, SIZE)
    noLoop()
    # グラフのデータ
    num1 = 47
    num2 = 90
    points_list.append(
        [spirograph(i, num1 / 100.0, num2 / 100.0) for i in range(360 * num1)]
    )


def draw():
    background(BACKGROUND_COLOR)
    strokeWeight(1)
    noFill()
    pushMatrix()
    translate(width / 2, height / 2)
    stroke(0, 255, 0, 50)
    for i in range(0, 360, 15):
        line(0, 0, 1000 * cos(radians(i)), 1000 * sin(radians(i)))
    for i in range(1, 1000, STEP):
        ellipse(0, 0, i * 2, i * 2)
    stroke(0, 255, 0)
    line(-2000, 0, 2000, 0)
    line(0, -2000, 0, 2000)
    # 大円
    stroke(255, 100)
    ellipse(0, 0, RADIUS * 2, RADIUS * 2)
    # グラフを描く
    for j, points in enumerate(points_list):
        stroke(colors[j][0], colors[j][1], colors[j][2])
        for i in range(len(points)):
            x1 = points[i][0]
            y1 = points[i][1]
            x2 = points[(i + 1) % len(points)][0]
            y2 = points[(i + 1) % len(points)][1]
            line(x1, -y1, x2, -y2)
    popMatrix()


def spirograph(i, a, b):
    t1 = radians(i)
    r1 = (1 - a) * RADIUS
    t2 = (1 - a) * t1 / float(a)
    r2 = a * b * RADIUS
    x = r1 * cos(t1) + r2 * cos(-t2)
    y = r1 * sin(t1) + r2 * sin(-t2)
    if i == 0:
        return x, y, a, b
    else:
        return x, y
spirograph_47_90

初めてのスピログラフを描いてみました。spirograph関数のところで、前節の計算結果を利用して、プロット点を返すようにしています。setup関数の中の num1, num2 を別の整数に変えることで、別のスピログラフを描くことができます。いろいろ数字を変えて遊んでください。

スピログラフのアニメーション

BACKGROUND_COLOR = color(0, 44, 77)
colors = [
    [255, 255, 255],  # white
    [255, 127, 0],  # orange
    [255, 0, 255],  # magenta
    [127, 127, 255],  # lightblue
    [255, 255, 0],  # yellow
    [0, 255, 0],  # lime
    [255, 127, 127],  # pink
    [127, 127, 127],  # gray
    [191, 191, 191],  # lightgray
    [0, 255, 255],  # cyan
    [127, 0, 127],  # purple
    [0, 0, 255],  # blue
    [127, 0, 0],  # brown
    [0, 127, 0],  # green
    [255, 0, 0],  # red
    [0, 0, 0],  # black
]
SIZE = 1000
STEP = 50
RADIUS = 450
points_list = []


def setup():
    size(SIZE, SIZE)
    # グラフのデータ
    num1 = 64
    num2 = 80
    points_list.append(
        [spirograph(i, num1 / 100.0, num2 / 100.0) for i in range(360 * num1)]
    )


def draw():
    background(BACKGROUND_COLOR)
    strokeWeight(1)
    noFill()
    pushMatrix()
    translate(width / 2, height / 2)
    stroke(0, 255, 0, 50)
    for i in range(0, 360, 15):
        line(0, 0, 1000 * cos(radians(i)), 1000 * sin(radians(i)))
    for i in range(1, 1000, STEP):
        ellipse(0, 0, i * 2, i * 2)
    stroke(0, 255, 0)
    line(-2000, 0, 2000, 0)
    line(0, -2000, 0, 2000)
    # 大円
    stroke(255, 100)
    ellipse(0, 0, RADIUS * 2, RADIUS * 2)
    # グラフを描く
    for j, points in enumerate(points_list):
        stroke(colors[j][0], colors[j][1], colors[j][2])
        # アニメーション
        a = points[0][2]
        b = points[0][3]
        count = min(frameCount - 1, len(points))
        for i in range(count):
            x1 = points[i % len(points)][0]
            y1 = points[i % len(points)][1]
            x2 = points[(i + 1) % len(points)][0]
            y2 = points[(i + 1) % len(points)][1]
            line(x1, -y1, x2, -y2)

        stroke(255, 100)
        t = radians(frameCount)
        r = (1 - a) * RADIUS
        x2 = points[frameCount % len(points)][0]
        y2 = points[frameCount % len(points)][1]
        small_circle_center = (r * cos(t), r * sin(t))
        # 小円
        ellipse(
            small_circle_center[0],
            -small_circle_center[1],
            a * RADIUS * 2,
            a * RADIUS * 2
        )
        # 腕1
        line(
            0,
            0,
            small_circle_center[0],
            -small_circle_center[1]
        )
        # 腕2
        line(
            x2,
            -y2,
            small_circle_center[0],
            -small_circle_center[1]
        )
        # 点
        fill(colors[j][0], colors[j][1], colors[j][2])
        ellipse(x2, -y2, 10, 10)
        noFill()
    popMatrix()


def spirograph(i, a, b):
    t1 = radians(i)
    r1 = (1 - a) * RADIUS
    t2 = (1 - a) * t1 / float(a)
    r2 = a * b * RADIUS
    x = r1 * cos(t1) + r2 * cos(-t2)
    y = r1 * sin(t1) + r2 * sin(-t2)
    if i == 0:
        return x, y, a, b
    else:
        return x, y
spirograph 64 80 animation

スピログラフは少しずつ出来上がっていく過程が楽しいので、いきなり完成すると感動が薄れます。そこで徐々にグラフを描いていくアニメーションを作画しました。a =0.64, b = 0.8 のスピログラフは、24枚の花弁になります。
(少し残念なのは、note にアップロードするために、60フレームに1枚しかキャプチャしていないことです。もっと滑らかに動かしたいときは、上記のコードを Processing3 (Python mode) に貼り付けて、自分のパソコンで動かしてみてください。)

a の値を連続的に変化させる

BACKGROUND_COLOR = color(0, 44, 77)
colors = [
    [255, 255, 255],  # white
    [255, 127, 0],  # orange
    [255, 0, 255],  # magenta
    [127, 127, 255],  # lightblue
    [255, 255, 0],  # yellow
    [0, 255, 0],  # lime
    [255, 127, 127],  # pink
    [127, 127, 127],  # gray
    [191, 191, 191],  # lightgray
    [0, 255, 255],  # cyan
    [127, 0, 127],  # purple
    [0, 0, 255],  # blue
    [127, 0, 0],  # brown
    [0, 127, 0],  # green
    [255, 0, 0],  # red
    [0, 0, 0],  # black
]
SIZE = 1000
STEP = 50
RADIUS = 450
points_list = []


def setup():
    size(SIZE, SIZE)

    # グラフのデータ
    step = 0.01
    for j in range(1, 50):
        if j % 10 == 0:
            k = j / 10
        else:
            k = j
        points_list.append(
            [spirograph(i, j * step, 80 * step) for i in range(360 * k)]
        )


def draw():
    background(BACKGROUND_COLOR)
    strokeWeight(1)
    noFill()
    pushMatrix()
    translate(width / 2, height / 2)
    stroke(0, 255, 0, 50)
    for i in range(0, 360, 15):
        line(0, 0, 1000 * cos(radians(i)), 1000 * sin(radians(i)))
    for i in range(1, 1000, STEP):
        ellipse(0, 0, i * 2, i * 2)
    stroke(0, 255, 0)
    line(-2000, 0, 2000, 0)
    line(0, -2000, 0, 2000)
    # 大円
    stroke(255, 100)
    ellipse(0, 0, RADIUS * 2, RADIUS * 2)
    # グラフを描く
    j = min(frameCount - 1, len(points_list) - 1)
    points = points_list[j]
    stroke(colors[j % 16][0], colors[j % 16][1], colors[j % 16][2])
    # すべての点をつなぐ
    for i in range(len(points)):
        x1 = points[i][0]
        y1 = points[i][1]
        x2 = points[(i + 1) % len(points)][0]
        y2 = points[(i + 1) % len(points)][1]
        line(x1, -y1, x2, -y2)
    popMatrix()

def spirograph(i, a, b):
    t1 = radians(i)
    r1 = (1 - a) * RADIUS
    t2 = (1 - a) * t1 / float(a)
    r2 = a * b * RADIUS
    x = r1 * cos(t1) + r2 * cos(-t2)
    y = r1 * sin(t1) + r2 * sin(-t2)
    if i == 0:
        return x, y, a, b
    else:
        return x, y
spirograph from 1 to 49

a の値を 0.01 から 0.49 まで連続で変化させてアニメーションにしました。外側から内側に描画範囲が広がっていく様子が観察できました。

# グラフのデータ
step = 0.01
# for j in range(1, 50):
for j in range(50, 100):
    if j % 10 == 0:
        k = j / 10
    else:
        k = j
    points_list.append(
        [spirograph(i, j * step, 80 * step) for i in range(360 * k)]
    )
spirograph from 50 to 99


a の値を0.5 から 0.99 まで連続で変化させたときのアニメーションです。0.5 は特異点で楕円が現れます。数字が大きくなるにしたがって、描画範囲が外側に小さくなっていきます。

スピログラフのジェネラティブアート

BACKGROUND_COLOR = color(0, 44, 77)
colors = [
    [255, 255, 255],  # white
    [255, 127, 0],  # orange
    [255, 0, 255],  # magenta
    [127, 127, 255],  # lightblue
    [255, 255, 0],  # yellow
    [0, 255, 0],  # lime
    [255, 127, 127],  # pink
    [127, 127, 127],  # gray
    [191, 191, 191],  # lightgray
    [0, 255, 255],  # cyan
    [127, 0, 127],  # purple
    [0, 0, 255],  # blue
    [127, 0, 0],  # brown
    [0, 127, 0],  # green
    [255, 0, 0],  # red
    [0, 0, 0],  # black
]
SIZE = 1000
STEP = 50
RADIUS = 450
points_list = []


def setup():
    size(SIZE, SIZE)

    # グラフのデータ
    step = 0.01
    for j in range(1, 100, 10):
        if j % 10 == 0:
            k = j / 10
        else:
            k = j
        points_list.append(
            [spirograph(i, j * step, 80 * step) for i in range(360 * k)]
        )


def draw():
    background(BACKGROUND_COLOR)
    strokeWeight(1)
    noFill()
    pushMatrix()
    translate(width / 2, height / 2)
    # 大円
    stroke(255, 100)
    ellipse(0, 0, RADIUS * 2, RADIUS * 2)
    # グラフを描く
    for j, points in enumerate(points_list):
        stroke(colors[j][0], colors[j][1], colors[j][2])
        # すべての点をつなぐ
        for i in range(len(points)):
            x1 = points[i][0]
            y1 = points[i][1]
            x2 = points[(i + 1) % len(points)][0]
            y2 = points[(i + 1) % len(points)][1]
            line(x1, -y1, x2, -y2)
    popMatrix()


def spirograph(i, a, b):
    t1 = radians(i)
    r1 = (1 - a) * RADIUS
    t2 = (1 - a) * t1 / float(a)
    r2 = a * b * RADIUS
    x = r1 * cos(t1) + r2 * cos(-t2)
    y = r1 * sin(t1) + r2 * sin(-t2)
    if i == 0:
        return x, y, a, b
    else:
        return x, y
spirograph generative art

最後に、ジェネラティブアートを描きます。上記コードを実行すると曼陀羅のような作品が現れました。何か引き込まれるような不気味な絵ですね。この穴をくぐると、どこにつながっているのだろうと思ってしまいました。

子供のころ楽しんだスピログラフをもう一度体験出来てよかったです。コンピュータを使えば、精密な画像を瞬間に描くことができることもわかりました。私にとってのコンピューターは、子供にとっての「おもちゃ」みたいなものだなと改めて感じました。

発展的課題 a > 1 のとき、a < 0 のとき


# グラフのデータ
num1 = 111
num2 = 80
points_list.append(
    [spirograph(i, num1 / 100.0, num2 / 100.0) for i in range(360 * num1)]
)
spirograph 111 80
# グラフのデータ
num1 = -10
num2 = 80
points_list.append(
    [spirograph(i, num1 / 100.0, num2 / 100.0) for i in range(360 * abs(num1))]
)
spirograph -10 80

発展的な課題について少し触れておきます。

いままで a の範囲を $${0 < a < 1}$$ の範囲で考えてきました。大円の中を小円が転がるので、スピログラフとしてはその範囲のみ考えればよいことになります。
しかし数式上は $${a < 0}$$ の範囲や、$${1 < a}$$ の範囲も対応できているのです。小円の半径がマイナスであったり、大円より大きくなることは考えにくいかもしれません。そういうときはグラフ化してみましょう。上記のグラフは $${a = 1.11, a = -0.1}$$ のときのグラフになります。$${0 < a < 1}$$ の範囲と連続的に変化するグラフが得られます。

a がどんどん小さくなるとどんな図形になるのか、どんどん大きくなるとどうなるか。ぜひご自分のパソコンでコードを実行して実験してみてください。


前の記事
Processing でグラフを描く⑦ フーリエ級数
次の記事
Processing でグラフを描く⑨ マンデルブロ集合

その他のタイトルはこちら


この記事が気に入ったらサポートをしてみませんか?