p5.jsで追憶の「スピログラフ」を再現する

With
p5.jsで追憶の「スピログラフ」を再現する はコメントを受け付けていません。

前のエントリで紹介した「Clock Wave」「ノイズの多いらせん」は、「ジェネラティブ・アート-Processingによる実践ガイド」の前半のほうで紹介されている作品だったのですが、

…これらの出力を見ていて、これまで何十年も澱の中に沈んでいた、子どものころの記憶が突然よみがえってきたので、今回はその話を。

あれは、僕がまだ幼稚園児だったか、小学校1、2年の頃だったか。近所に僕と同い年か、ちょっと下か、くらいの女の子が住んでいました。母親同士が知り合いだったもので、時々連れられてその子の家に遊びに行くことがあったんです。

母親同士が話し込んでいる間、僕とその子は子ども部屋で一緒に遊んでいたのですが、ある日その子が「これ面白いから一緒にやろう」と、あるおもちゃを貸してくれました。そのおもちゃは、歯車のような形が抜かれたプラスチックの板に、これまたプラスチックでできた歯車をはめ込み、はめ込んだ歯車に開いた複数の小さな穴のどこかにボールペンを突っ込んでグリグリ回すと、キレイな花のような模様が描ける…というものでした。

歯車の種類はいっぱいあって、中には三角形や手裏剣のような形をしたものも。歯車をはめる板の穴も大小さまざま。これらの組み合わせと、ボールペンの先を突っ込む穴の違いで、まったく違うパターンの花模様が描けました。

そのおもちゃがすっかり気に入った僕は、家に帰ってから「○○ちゃんちにある、花の絵が描けるおもちゃがほしい」と両親に訴えたのですが「女の子が遊ぶようなおもちゃは、すぐ飽きるからダメ」みたいな理由で却下されてしまい、その後も、その子のうちに遊びにいった時だけ、それで遊ぶことができたのでした。

そのうち、その子のうちに行くこともなくなり、あのおもちゃのこともすっかり忘れて約40年(!)がたったのですが、「ジェネラティブ・アート」を読んでいて、そのおもちゃで描いた「花」の姿が急に意識にのぼってきたのです。

…「あの花、p5.jsで描けるんじゃね?」

スピログラフで描く「トロコイド曲線」

とりあえず「あのおもちゃ」が何だったのか調べようと、Google先生に「穴の空いた歯車にボールペン突っ込んで回すと花の絵が描けるやつ」と聞いてみたら、一発でわかりました。

「ローリングルーラー」あるいは「スピログラフ」という名称で、今も現役で売られているようです。スゴいぞGoogle先生。

そうそう! これこれ! 懐かしいなぁ。

さらに「スピログラフ」というのは(モノポリーとか、人生ゲームとか、ジェンガとか、ツイスターとかでも有名な)「ハズブロ」の登録商標であることもWikipediaから分かりました。

スピログラフ-Wikipedia

スピログラフで描けるあの花の絵は「内トロコイド曲線」と呼ばれるもののようです。

トロコイド-Wikipedia

…なんか、いろいろ数式が出てきましたよ。つーか、何が「女の子が遊ぶようなおもちゃ」だよ。めちゃくちゃ理系チックじゃねーか。センスねーな、俺の親。

ただ、理屈は非常にシンプルです。これなら、p5.jsで再現できそう。再現して、幼少期にスピログラフを買ってもらえなかった悲しい思い出にケリをつけてやろうではないですか。

追憶のスピログラフ powered by p5.js

で、先週のことになるのですが、スピログラフ風のトロコイド曲線を描けるプログラムを作ってみました(スマホブラウザでも一応動いていますが、キー操作が必要なのでキーボードがある環境での閲覧をオススメします)。

●[DEMO]追憶のスピログラフ(内トロコイド版) (クリックすると別ウインドウが開きます)

↑をクリックすると、ブラウザの別ウインドウにこんな感じの画面が出てきます。

大きな円の中で、小さな円が回っており、さらにその小さな円の外周部近くで色のついた小さな丸が回っているのがわかるでしょうか。その下に4本のスライドバーも出ているのですが、それについては後で説明します。

大きな円が「歯車状に抜かれた穴」、小さな円が「歯車」、色の付いた小さな丸が「ペン先」を表しています。この画面を「歯車モード」とします。

まず、ブラウザがアクティブになっている状態で「スペースキー」を押してみてください。画面が切り替わり「軌跡モード」になります。先ほどの歯車モードで表示されていた画面から「大きな円」と「小さな円」が消え、「色の付いた小さな丸」のみが軌跡を残しながら移動しはじめます。この状態でもう一度「スペースキー」を押すと、軌跡は消え、歯車モードに戻ります。

とりあえず「軌跡モード」にして、2~3分ほったらかしてみてください。ペン先の色は色相環に準じて「じわー」と変化していますので、しばらくするとこんな軌跡が描けているはずです。

モードを切り替えたり、ブラウザを閉じたりすると軌跡は消えてしまうので、もし気に入った模様ができたら、その場で「P」キーを押してみてください。そのタイミングで画面上にある模様がPNGの画像ファイルとしてダウンロードできます(Edge、IEだと、Pキーによる画像保存がうまく動かないようです。Chrome、Firefoxで動作確認しています)。

しばらく眺めていると、「円のサイズやペンの位置を変えてみたい」と思うかもしれません。そこで、下の4本のスライドバーの出番です。各バーを動かすと、以下の要素をリアルタイムに変化させることができます。

「歯車モード」の状態で、スライドバーを動かして各サイズを調整し、決定したら「軌跡モード」に切り替えて、どんな絵ができあがるのかを観察し、気に入ったら「P」キーで保存する…といった感じで使ってみてください。実はこのスライドバーは「軌跡モード」でも有効なのですが、円が表示されていないと、変化がよくわかりません。

また、おもちゃのスピログラフでは、物理的な理由で「内側の歯車は、外側の円より必ず小さい」とか「ペン先は内側の円の中になければならない」といった制限がありますが、このプログラムでは「外の円より中の円が大きい場合」とか「ペン先が内側、あるいは外側の円よりもさらに外側にある場合」なども設定できます。ちょっとスライドバーをずらすだけで、全然違う絵ができあがってきますので、ぜひいろいろ試してみてください。

あと、トロコイド曲線には「内トロコイド」以外に、固定された円の「外周」を別の円が回っていく「外トロコイド」もあります。スピログラフでは、型抜きされた板を使わずに「2つの歯車」を組み合わせることで描ける曲線です。今回、ついでに「外トロコイド」バージョンも作ってみました。

●[DEMO]追憶のスピログラフ(外トロコイド版) (クリックすると別ウインドウが開きます)

操作方法は、内トロコイド版と同じですが、また違ったパターンが出てきます。

座標は変えず紙を動かす「translate」「rotate」

プログラムの話ですが、今回は円の回転を表現するのにsinやcosを使わず、p5.jsにある「translate」と「rotate」という関数を駆使してみました。

この2つの関数。初めて使うときにはちょっと面食らうのですが、要は絵を描く際に「筆の座標は動かさずに、紙の方を動かす」という操作をします。

たとえば、「point(0, 0);」と書いて、画面上で(0, 0)の位置に点を打ち、次に「translate(10, 0);」とした後、再び「point(0, 0);」を実行すると2つ目の点はスケッチ上で「(10, 0)」の位置に打たれます。「rotate」は同様に、紙を(その時点での)「(0, 0)」を中心として指定した角度だけ回転させます。

今回の2本のプログラムでは、実は毎フレーム「全く同じ座標に、同じ絵」を描いているのですが、内トロコイドの場合

・フレームごとに1度ずつ角度を増やしながら「rotate」で紙を回転させる

・外側の円を描く

・「translate」で紙の中心を内側の円の中心に移動する

・「rotate」で紙を回転させて内側の円を描く(回転角度は外側の円と内側の円の円周の比で決まる)

・「ペン先」を描く

という処理を繰り返すことで、動きを表現しています。文章で書いても良く分からないと思うので、最後にソースコードを付けておきます。ちなみにこれ、表示する円がいずれも正円だからこそできる書き方です。スピログラフには「ラグビーボール」や「三角おにぎり」のような形をした歯車も用意されていたように思うのですが、これらを再現するには、この方法じゃ無理ですね。かなり複雑な式を書かなければならなくなるはずなので、そのうち、気が向いたらチャレンジしてみるかもしれません。

トロコイド曲線の描画にランダム要素は一切ないのですが、内側の円と、外側の円の大きさ、そしてペン先の位置、大きさ、色の変化で無限のパターンが生まれます。もしお気に召したようでしたら、さらに「背景色」や「ペン先の色の変化」などをプログラム上で変更して試してみてください。特にペンの色を「モノクロ」にしてみると、また独特の雰囲気がある絵になるのでオススメです。なお、スライドバーの実現にあたって「p5.dom.js」を追加のライブラリとして使っていますのでご注意ください。

この2つのプログラム、1日で一気に書いたのですが、画面上にできあがっていく「花」のようすを眺めていたら、スピログラフを買ってもらえなかった幼少期の悲しい記憶が、少しずつ薄れていくような気がしました。あの時の自分に「まぁ、そんなに落ち込むな。おまえ、40年くらいしたらパソコン使って、同じようなおもちゃ、自分で作れるようになるから」と声を掛けてやりたい気分です(笑)。

●内トロコイド版のソースコード(要p5.js、p5.dom.js)

var _checkmode = true;
var _extrad = 250; //外円の半径
var _intrad = 123; //内円の半径
var _intpen = 100; //ペンの座標(内円中心からの距離)
var _penweight = 6; //ペンの太さ
var _extangle = 0;
var _extanglestep = 1; //内円の移動角度/フレーム
var _intangle = 0;
var _extcircle, _intcircle, _pencoord, _intanglestep;
objSet();
var _bgcol = 10; //背景色(0-255)
var _col = 0; //モノクロ時は不要、カラー時のスタート色(H)
var _colstep = 0.03; //色の変化単位/フレーム
var extrad_slider, intrad_slider, intpen_slider, penweight_slider;

function objSet() {
    _extcircle = new PointObj(0, 0, _extrad)
    _intcircle = new PointObj(0, _extrad-_intrad, _intrad)
    _pencoord = new PointObj(0, _intpen);
    _intanglestep = -(_extrad/_intrad)*_extanglestep;
}

function setup() {
    colorMode(HSB); //モノクロ時は不要
    createCanvas(600, 600);
    setSlider();
    background(_bgcol);
    noStroke();
}

function draw() {
    if (_checkmode) { background(_bgcol); }
    translate(width/2, height/2);
    rotate(radians(_extangle));
    if (_checkmode) {
        stroke(255);
        fill(20);
        ellipse(_extcircle.x, _extcircle.y, _extcircle.radius*2);
        fill(_bgcol);
        ellipse(0,0,10);
    }
    _extangle += _extanglestep; 
    translate(_intcircle.x, _intcircle.y);
    rotate(radians(_intangle));
    _intangle += _intanglestep;
    if (_checkmode) {
        stroke(255);
        fill(20);
        ellipse(0, 0, _intcircle.radius*2);
        fill(_bgcol);
        ellipse(0,0,10);
    }
    noStroke();
    fill(_col, 100, 100, 0.25); //モノクロのときは、ここをfill(255, 70);とかに
    ellipse(_pencoord.x, _pencoord.y, _penweight);
    _col += _colstep; //色を変えない時は不要
    if(_col>360) { _col=0;} //色を変えない時は不要
    sliderValue();
}

function PointObj(ex, why, radi) {
    this.x = ex;
    this.y = why;
    this.radius = radi;
}

function setSlider() {
    extrad_slider = createSlider(20, 280, _extrad);
    extrad_slider.position(5, height+10);
    extrad_slider.size(300);
    intrad_slider = createSlider(20, 280, _intrad);
    intrad_slider.position(5, height+40);
    intrad_slider.size(300);
    intpen_slider = createSlider(10, 300, _intpen);
    intpen_slider.position(5, height+70);
    intpen_slider.size(300);
    penweight_slider = createSlider(2, 30, _penweight);
    penweight_slider.position(5,height+100);
    penweight_slider.size(300);
}

function sliderValue() {
    _extrad = extrad_slider.value();
    _intrad = intrad_slider.value();
    _intpen = intpen_slider.value();
    _penweight = penweight_slider.value();
    objSet();
}

function keyPressed() {
    if (keyCode === 80) {
        saveCanvas();
    }
    if (keyCode === 32) {
        background(_bgcol);
        _checkmode=!_checkmode;
    }
}

●外トロコイド版のソースコード(要p5.js、p5.dom.js)

var _checkmode = true;
var _extrad = 71; //中央円の半径
var _intrad = 93; //回転円の半径
var _intpen = 79; //ペンの座標(回転円中心からの距離)
var _penweight = 6; //ペンの太さ
var _extangle = 0;
var _extanglestep = 1; //回転円の移動角度/フレーム
var _intangle = 0;
var _extcircle, _intcircle, _pencoordk, _intanglestep;
objSet();
var _bgcol = 10; //背景色(0-255)
var _col = 0; //モノクロ時は不要、カラー時のスタート色(H)
var _colstep = 0.03; //色の変化単位/フレーム

function objSet() {
    _extcircle = new PointObj(0, 0, _extrad)
    _intcircle = new PointObj(0, _extrad+_intrad, _intrad)
    _pencoord = new PointObj(0, _intpen);
    _intanglestep = 1/(_intrad/_extrad)*_extanglestep;
}

function setup() {
    colorMode(HSB); //モノクロ時は不要
    createCanvas(600, 600);
    setSlider();
    background(_bgcol);
    noStroke();
}

function draw() {
    if (_checkmode) { background(_bgcol); }
    translate(width/2, height/2);
    rotate(radians(_extangle));
    if (_checkmode) {
        stroke(255);
        fill(20);
        ellipse(_extcircle.x, _extcircle.y, _extcircle.radius*2);
        fill(_bgcol);
        ellipse(0,0,10);
    }
    _extangle += _extanglestep; 
    translate(_intcircle.x, _intcircle.y);
    rotate(radians(_intangle));
    _intangle += _intanglestep;
    if (_checkmode) {
        stroke(255);
        fill(20);
        ellipse(0, 0, _intcircle.radius*2);
        fill(_bgcol);
        ellipse(0,0,10);
    }
    noStroke();
    fill(_col, 100, 100, 0.3); //モノクロの時は、colorModeをコメントアウトし、ここをfill(200,25);とかに
    ellipse(_pencoord.x, _pencoord.y, _penweight);
    _col += _colstep; //色を変えない時は不要
    if(_col>360) { _col=0;} //色を変えない時は不要
    sliderValue();
}

function PointObj(ex, why, radi) {
    this.x = ex;
    this.y = why;
    this.radius = radi;
}

function setSlider() {
    extrad_slider = createSlider(10, 200, _extrad);
    extrad_slider.position(5, height+10);
    extrad_slider.size(300);
    intrad_slider = createSlider(10, 280, _intrad);
    intrad_slider.position(5, height+40);
    intrad_slider.size(300);
    intpen_slider = createSlider(10, 300, _intpen);
    intpen_slider.position(5, height+70);
    intpen_slider.size(300);
    penweight_slider = createSlider(2, 30, _penweight);
    penweight_slider.position(5,height+100);
    penweight_slider.size(300);
}

function sliderValue() {
    _extrad = extrad_slider.value();
    _intrad = intrad_slider.value();
    _intpen = intpen_slider.value();
    _penweight = penweight_slider.value();
    objSet();
}


function keyPressed() {
    if (keyCode === 80) {
        saveCanvas();
    }
    if (keyCode === 32) {
        background(_bgcol);
        _checkmode=!_checkmode;
    }
}