幾何学計算アニメーションを作ってみよう

第3回JavaScript座標回転プログラムをPHPサーバで動画にしてみよう

前回は、座標計算の説明をしました。座標をドラッグすると、JavaScriptで結果が見れるようにしました。

せっかくJavaScriptでマウスのドラッグを受け付けているのですから、これがそのまま動画にできたら便利ですね。今回はこれに挑戦してみましょう。

JavaScript座標回転プログラムの動作

まず、前回のvector.htmlを見てみましょう。divタグで作られたテキストは、スタイルがposition:absolute;になっています。ですから、スタイルのleftとtopに値を指定することで、任意の位置に動かすことができます。昔はこういうのをスプライトと呼んでいた時代もありました。

リスト1> vector.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">
<!-- copy from: http://musicgroupwork.at.webry.info/200701/article_2.html -->
<!-- copy from: http://jsm.suepon.com/script/jsm31.html -->
<html>
<head>
<script type="text/javascript"><!--

handlelist = null;
vectorlist = new Array();


function    vector(container, arrowsize, width, color) {
    this.sx = 0;
    this.sy = 0;
    this.ex = 0;
    this.ey = 0;
    this.polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
    this.polygon.setAttribute("stroke", color);
    this.polygon.setAttribute("fill", "none");
    this.polygon.setAttribute("stroke-width", width);
    container.appendChild(this.polygon);
    
    this.setup = function() {
        this.calc();
        
        var    vx = this.ex - this.sx;
        var    vy = this.ey - this.sy;
        var    point = this.sx + ", " + this.sy;
        point += ", " + this.ex + ", " + this.ey;
        if (vx || vy) {
            var    length = Math.sqrt(vx * vx + vy * vy);
            vx = vx * arrowsize / length;
            vy = vy * arrowsize / length;
            point += ", " + (this.ex - vx - vy / 2);
            point += ", " + (this.ey - vy + vx / 2);
            point += ", " + (this.ex - vx + vy / 2);
            point += ", " + (this.ey - vy - vx / 2);
            point += ", " + this.ex + ", " + this.ey;
        }
        this.polygon.setAttribute("points", point);
    };
    this.calc = function() {
    };
}


function    setupvector() {
    for (var i=0; i<vectorlist.length; i++)
        vectorlist[i].setup();
}


function    setuphandle(obj) {
    obj.onmousedown = function(e) {
        var    offsetX = e.pageX - parseInt(obj.style.left);
        var    offsetY = e.pageY - parseInt(obj.style.top);
        
        document.onmousemove = function(e) {
            obj.style.left = (e.pageX - offsetX) + 'px';
            obj.style.top = (e.pageY - offsetY) + 'px';
            setupvector();
            return false;
        };
        document.onmouseup = function(e) {
            document.onmousemove(e);
            document.onmouseup = null;
            document.onmousemove = null;
            return false;
        };
        document.onmousemove(e);
        return false;
    };
}


window.onload = function() {
    var    obj;
    var    containerObj = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    document.getElementById("canvas").appendChild(containerObj);
    
    handlelist = document.getElementById("canvas").getElementsByTagName("div");
    for (var i=0; i<handlelist.length; i++)
        setuphandle(handlelist[i]);
    
    vectorlist.push(obj = new vector(containerObj, 10, 4, "#00f"));
    obj.calc = function() {
        this.sx = 200;
        this.sy = 200;
        this.ex = parseInt(handlelist[2].style.left);
        this.ey = 200;
    };
    vectorlist.push(obj = new vector(containerObj, 10, 4, "#f00"));
    obj.calc = function() {
        this.sx = parseInt(handlelist[2].style.left);
        this.sy = 200;
        this.ex = parseInt(handlelist[2].style.left);
        this.ey = parseInt(handlelist[2].style.top);
        handlelist[2].innerHTML = "+ (" + ((this.ex - 200) / 100.0) + ", " + ((200 - this.ey) / 100.0) + ")";
    };
    vectorlist.push(obj = new vector(containerObj, 15, 1, "#0ff"));
    obj.calc = function() {
        this.sx = 600;
        this.sy = 200;
        this.ex = parseInt(handlelist[0].style.left);
        this.ey = parseInt(handlelist[0].style.top);
        handlelist[0].innerHTML = "+ (" + ((this.ex - 600) / 100.0) + ", " + ((200 - this.ey) / 100.0) + ")";
    };
    vectorlist.push(obj = new vector(containerObj, 15, 1, "#fa0"));
    obj.calc = function() {
        this.sx = 600;
        this.sy = 200;
        this.ex = parseInt(handlelist[1].style.left);
        this.ey = parseInt(handlelist[1].style.top);
        handlelist[1].innerHTML = "+ (" + ((this.ex - 600) / 100.0) + ", " + ((200 - this.ey) / 100.0) + ")";
    };
    vectorlist.push(obj = new vector(containerObj, 10, 4, "#00f"));
    obj.calc = function() {
        var    vx0 = parseInt(handlelist[0].style.left) - 600;
        var    vy0 = parseInt(handlelist[0].style.top) - 200;
        
        this.sx = this.ex = 600;
        this.sy = this.ey = 200;
        this.ex += (parseInt(handlelist[2].style.left) - 200) * vx0 / 100;
        this.ey += (parseInt(handlelist[2].style.left) - 200) * vy0 / 100;
    };
    vectorlist.push(obj = new vector(containerObj, 10, 4, "#f00"));
    obj.calc = function() {
        var    vx0 = parseInt(handlelist[0].style.left) - 600;
        var    vy0 = parseInt(handlelist[0].style.top) - 200;
        var    vx1 = parseInt(handlelist[1].style.left) - 600;
        var    vy1 = parseInt(handlelist[1].style.top) - 200;
        
        this.sx = 600;
        this.sy = 200;
        this.sx += (parseInt(handlelist[2].style.left) - 200) * vx0 / 100;
        this.sy += (parseInt(handlelist[2].style.left) - 200) * vy0 / 100;
        this.ex = this.sx;
        this.ey = this.sy;
        this.ex += (200 - parseInt(handlelist[2].style.top)) * vx1 / 100;
        this.ey += (200 - parseInt(handlelist[2].style.top)) * vy1 / 100;
    };
    setupvector();
}


// --></script>
<title>vector operation</title>
</head>
<body>
<div id="canvas" style="width:800px; height:400px;">
    <div style="position:absolute; left:700px; top:200px;">+</div>
    <div style="position:absolute; left:600px; top:100px;">+</div>
    <div style="position:absolute; left:250px; top:150px;">+</div>
</div>
<HR>
</body>
</html>

setuphandle関数では、このdivタグにイベントハンドラを登録しています。手間を省くため、クロージャとコールオブジェクトを用いて、setuphandleに渡されたobjをonmousedownの中で使っています。onmousedownのときのマウスの座標をoffsetXとoffsetYという形で保存し、これもonmousemoveの中で使っています。ちなみにparseIntは、スタイルにつく「px」を取り除いて数値にするのに利用しています。

ドラッグされたときは、setupvector関数を呼んで、すべての矢印を計算しなおしています。矢印はオブジェクトを使っており、calcメソッドの中に、矢印ごとの個別の計算処理をあとから登録してあります(JavaScriptでは、new 関数名()とすると、オブジェクトが作られてから関数名()がコンストラクタとして呼ばれます⁠⁠。矢印のものは多角形(SVGのpolygon)で描いています。矢印の矢の座標は、前回解説した座標計算で求めています。

マウス操作をPHPサーバで記録するには

さて、これを動画にしようとするわけですが、JavaScriptでは画像が作れません。そこで、座標をサーバのPHPに渡して、サーバ側で画像を作ることにします。これには、いくつかのやり方があります。1つ1つ見ていきましょう。

1つ目の方法は、ドラッグ操作と平行してサーバにリクエストを出し、リアルタイムに動画を作る方法です。今をときめくAjaxというやつです。ですが、これはとても処理がややこしくなります。個人的には、他の方法がないかを調べておきたいところです。

2つ目の方法は、ドラッグ中の情報をフォームに書き出しておき、あとでまとめて送信する方法です。リアルタイムにはなりませんが、フォームをチェックすればデバッグも簡単ですし、wikiなどに出力するような拡張もできます。

もう1つのポイントは、サーバ側に渡すデータの抽象度です。矢印の始点と終点の座標を記録するのが一案です。この方法だと、PHP側ではJavaScript側と同じやり方で矢印の矢の座標を計算する必要があります。つまり、同じことをするコードを、PHP版とJavaScript版の2つ用意して保守しなければならないということです。類似の案として、オブジェクトをシリアライズして渡すという方法があります。

別のやり方としては、計算済みの多角形(polygon)の座標をPHP側に渡す方法もあります。JavaScript側では矢印の始点と終点から矢の座標を計算し、多角形のSVGにしていますが、PHP側はこれをimagefilledpolygonなどに渡すことになります。コードの重複がないので、JavaScript側のコードで矢の形を変えれば、PHP側にも自動的に反映されることになります。ただし、この多角形が矢印であるといった情報は失われてしまいますので、PHP側で特定の矢印だけ形を変えたい、といった対応はできなくなります。

これは喩えていうと印刷のときに文字データを渡すか、ビットマップにしたデータを渡すかのような違いです。文字データを渡す場合、両方に同じフォントが必要になります。ビットマップの場合、追加の加工をしたい場合などに制限が出てきます。

どちらも一長一短あるのですが、今回はデータの送信はフォームを使ってまとめておこなうようにし、送るデータは多角形の座標としました。理由ですが、少なくとも最初のうちはできるだけシンプルにしたく、またコードの重複も避けたいと考えたからです。具体的なやり方ですが、フォームを使ってテキストエリアを送信します。テキストエリアの内容は、時刻、多角形、テキストが1行ごとに並んでいます。PHP側ではこれを読み、画像を生成します。

まず、これがうまく動くかを見てみましょう。送信ボタンを作り、これを押したときにそのときの座標データを生成して送信します(formのonsubmitを使っています⁠⁠。PHP側ではここから画像を作り、ブラウザに返します。

図1 vector3.phpの動作① ドラッグした時点ではまだ入力されません
図1 vector3.phpの動作① ドラッグした時点ではまだ入力されません
図2 vector3.phpの動作② 送信した瞬間に入力されます
図2 vector3.phpの動作② 送信した瞬間に入力されます
図3 vector3.phpの動作③ PHP側で作った画像が表示されます
図3 vector3.phpの動作③ PHP側で作った画像が表示されます

うまくいったら、次はドラッグの都度テキストエリアに出力するようにしてみましょう。テキストエリアにはドラッグが完了するたびに座標データが追記されていきますので、デバッグはしやすいと思います。PHP側は複数の画像を生成することになりますから、画像はファイルに出力し、IMGタグを並べたHTMLをブラウザに返すようにしました。実はこのPHPは、前回の画面キャプチャで使ったものです。

図4 vector4.phpの動作① ドラッグの都度、入力されます
図4 vector4.phpの動作① ドラッグの都度、入力されます
図5 vector4.phpの動作② 1つずつの画像が生成され、リンクが表示されます
図5 vector4.phpの動作② 1つずつの画像が生成され、リンクが表示されます

なお、送信したあとに、別の操作をして再び送信すると、前回生成したファイルは上書きされてしまいます。サーバサイドではありますが、これは同時に1人しか使えませんので、webで公開するには向きません。サーバ側に画像ファイルを作る手段と考えるとよいと思います。

最後に、ドラッグの経過をリアルタイムに出力するバージョンです。onmousemoveのたびにテキストエリアを書き換えると遅くなりますので、文字列は変数に保持しておき、送信時にテキストエリアに入れるようにしました。PHP側は、時刻データを見て同じ画像をいくつも出力し、マウス操作を再現するようにしています。

図6 vector5.phpの動作① ドラッグ中の動きも記録されています
図6 vector5.phpの動作① ドラッグ中の動きも記録されています
図7 vector5.phpの動作② 送信した瞬間に入力されます。コマ数が多いのでスクロールバーが小さくなっています
図7 vector5.phpの動作② 送信した瞬間に入力されます。コマ数が多いのでスクロールバーが小さくなっています
図8 vector5.phpの動作③ 1コマずつの画像が生成されます
図8 vector5.phpの動作③ 1コマずつの画像が生成されます

今回はマウスカーソルは表示しませんでしたが、簡単に表示できるはずです。たとえばボタン操作をわかりやすく表示するなど、いろいろな応用が考えられます。ぜひ、うまく利用してみてください。

おすすめ記事

記事・ニュース一覧