roombaの日記

読書・非線形科学・プログラミング・アート・etc...

テオ・ヤンセン機構をHTML5 Canvasでアニメーションに

はじめに

オランダのアーティストであるTheo Jansen(テオ・ヤンセン)氏は、風力によって歩行する巨大な脚「ストランドビースト」をつくったことで有名です。「ストランドビースト」には特殊なリンク機構(Jansen's Linkage)が用いられており、これによって風力による回転運動を生物のように生き生きとした脚の動きに変換しています。


Theo Jansen Japan
Jansen's linkage - Wikipedia, the free encyclopedia

以下のような組み立てキットも発売されています。私も持っていますが楽しいです。

この動きをアニメーションにしてみようというのが今回のテーマです。
下記記事と同様にHTML5 Canvasを用い、ブラウザ上で動くようにします。

ヤンセンのリンク機構

ヤンセンのリンク機構は以下のような構造になっています(図は片足のみ)。黒字がリンクの名前a~i, 青字が点の名前A~Gを表し、緑色は補助線です。各リンクの長さは決まっていて、Wikipediaなどでみることができると思います。本記事末尾のソースコードにも定義されています。
点Oと点Bは固定されており、点Oまわりにリンクm(線分OA)が回転するのに応じて全体が動くようになっています。
f:id:roomba:20150225142033j:plain:w300

アニメーション

アニメーションは以下の手順によって行うことができます。

  1. 座標系の原点を点Oとする
  2. リンクm(線分OA)の角度Θを決める
  3. Θに応じて点Aの座標が決まり、頑張って計算すれば他の点の座標も求まる
  4. 上記の座標に応じてリンクを描画
  5. 角度Θを少し増加させ、3に戻る(ループ)

上記の「頑張って計算すれば」というところが曲者です。私は頑張って計算しましたが、図解するのは大変そうなので今後時間があれば追記します…
余弦定理とかの高校数学を使います。

追記:詳細説明の記事を追加しました。

テオ・ヤンセン機構の計算【詳細版】 - roombaの日記


先ほどの図は片足のみでしたが、図の右側にも同様の計算から反対側の脚をおくことができます。さらに、位相を120°ずつずらして3ペアの脚を同時に描画すると以下のようなアニメーションが得られます。

歩いてる!

ソースコード

汚いソースコードを以下に示します。
「頑張って計算」の内容がdraw_jansen関数に詰め込まれているのですが、大変なので解読はあまりおすすめしません。
自由にご利用ください。

<canvas id="canvas1"></canvas>
<script type="text/javascript">
var c1 = document.getElementById("canvas1");
c1.width = 350;
c1.height = 300;
var ctx = c1.getContext("2d");

// ---------- 変数 ---------- 
// リンクの長さ
var a = 38;  var b = 41.5;
var c = 39.3; var d = 40.1;
var e = 55.8; var f = 39.4;
var g = 36.7; var h = 65.7;
var i = 49;  var j = 50;
var k = 61.9; var l = 7.8;
var m = 15;
// 各点の座標
var O = [0.0, 0.0]; var A = [0.0, 0.0];
var B = [-a, -l];  var C = [0.0, 0.0];
var D = [0.0, 0.0]; var E = [0.0, 0.0];
var F = [0.0, 0.0]; var G = [0.0, 0.0];
// 原動節の角度(これを時間とともに変化)
var theta = 0;

// ---------- main ----------  
function main(){
    setInterval(display, 10);// displayを定期的に実行
}
// 実行
main();

// ---------- その他の関数 ---------- 
// (x1, y1)から(x2, y2)に線を引く関数
function draw_link(x1, y1, x2, y2){
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(1.5*x1+175, -1.5*y1+150);// 本当は括弧内を(x1, y1)としたいけど、描画領域・向き・大きさの都合で
    ctx.lineTo(1.5*x2+175, -1.5*y2+150);// 上の行と同様
    ctx.closePath();
    ctx.stroke();
}

// 原動節mの角度をもとにリンク機構を描画する関数
// 第二引数signに+1か-1をいれることで、画面左側or右側のどちらの脚を描画するか選択
function draw_jansen(theta, sign){
    var theta_ab, theta_bc, theta_de, theta_df;
    var xc, yc;
    var AB, DE;
    // link j,b
    AB = Math.sqrt( (a+A[0])*(a+A[0]) + (l+A[1])*(l+A[1]) );
    xc = (AB*AB + b*b - j*j) / (2.0*AB);
    yc = Math.sqrt( b*b - xc*xc );
    theta_ab = Math.atan2(A[1]-B[1], A[0]-B[0]);
    C[0] = -a + xc*Math.cos(theta_ab) + yc*Math.cos(theta_ab+Math.PI/2);
    C[1] = -l + xc*Math.sin(theta_ab) + yc*Math.sin(theta_ab+Math.PI/2);
    draw_link(sign*A[0], A[1], sign*C[0], C[1]);// link j
    draw_link(sign*B[0], B[1], sign*C[0], C[1]);// link b
    // link e,d
    xc = (b*b + d*d - e*e) / (2.0*b);
    yc = Math.sqrt( d*d - xc*xc );
    theta_bc = Math.atan2(C[1]-B[1], C[0]-B[0]);
    E[0] = B[0] + xc*Math.cos(theta_bc) + yc*Math.cos(theta_bc+Math.PI/2);
    E[1] = B[1] + xc*Math.sin(theta_bc) + yc*Math.sin(theta_bc+Math.PI/2);
    draw_link(sign*E[0], E[1], sign*C[0], C[1]);// link e
    draw_link(sign*B[0], B[1], sign*E[0], E[1]);// link d
    // link k,c
    xc = (AB*AB + c*c - k*k) / (2.0*AB);
    yc = Math.sqrt( c*c - xc*xc );
    D[0] = B[0] + xc*Math.cos(theta_ab) + yc*Math.cos(theta_ab-Math.PI/2);
    D[1] = B[1] + xc*Math.sin(theta_ab) + yc*Math.sin(theta_ab-Math.PI/2);
    draw_link(sign*A[0], A[1], sign*D[0], D[1]);// link k
    draw_link(sign*B[0], B[1], sign*D[0], D[1]);// link c
    // link f,g
    DE = Math.sqrt( (D[0]-E[0])*(D[0]-E[0]) + (D[1]-E[1])*(D[1]-E[1]) );
    xc = (DE*DE + g*g - f*f) / (2.0*DE);
    yc = Math.sqrt( g*g - xc*xc );
    theta_de = Math.atan2(E[1]-D[1], E[0]-D[0]);
    F[0] = D[0] + xc*Math.cos(theta_de) + yc*Math.cos(theta_de+Math.PI/2);
    F[1] = D[1] + xc*Math.sin(theta_de) + yc*Math.sin(theta_de+Math.PI/2);
    draw_link(sign*F[0], F[1], sign*D[0], D[1]);// link g
    draw_link(sign*E[0], E[1], sign*F[0], F[1]);// link f
    // link h,i
    xc = (g*g + i*i - h*h) / (2.0*g);
    yc = Math.sqrt( i*i - xc*xc );
    theta_df = Math.atan2(F[1]-D[1], F[0]-D[0]);
    G[0] = D[0] + xc*Math.cos(theta_df) + yc*Math.cos(theta_df+Math.PI/2);
    G[1] = D[1] + xc*Math.sin(theta_df) + yc*Math.sin(theta_df+Math.PI/2);
    draw_link(sign*F[0], F[1], sign*G[0], G[1]);// link h
    draw_link(sign*D[0], D[1], sign*G[0], G[1]);// link i
}

// 左側と右側の両脚のペアを描画する関数
function draw_pair(theta){
    // 片側の脚のリンク機構を描画
    A[0] = m * Math.cos(theta);
    A[1] = m * Math.sin(theta);// 原動節mの端Aの座標
    draw_link(O[0], O[0], A[0], A[1]);// 原動節m(=OA)を描画
    draw_jansen(theta, 1);// リンク機構を描画
    // 反対側の脚のリンク機構を描画
    A[0] = m * Math.cos(Math.PI-theta);
    A[1] = m * Math.sin(Math.PI-theta);
    draw_jansen(Math.PI-theta, -1);// 第二引数を-1にすることで反対側の脚を選択
}  

function display(){
    var omega = 0.05;// 角速度
    // 120度ごとにずらし、それぞれ色を変えて描画する
    ctx.clearRect(0, 0, 350, 300);// 残像を消す
    ctx.strokeStyle = "rgb(255, 100, 100)";
    draw_pair(theta);
    ctx.strokeStyle = "rgb(100, 255, 100)";
    draw_pair(theta+Math.PI/1.5);
    ctx.strokeStyle = "rgb(100, 100, 255)";
    draw_pair(theta+2*Math.PI/1.5);

    theta += omega;
}
</script>