HTML5のCanvasを試す

今更ですが、HTML5Canvasを試してみたいと思います。


次期HTMLの規格であるHTML5では、Canvasというものを使って直接図形が描画できるようになりました。
描画速度は、というとそれほど速いわけではないですが、描画の多いものでなければ十分な速度で動くゲームも作れたりします。
(HTML5関連の詳しい資料はHTML5.JP - 次世代HTML標準 HTML5情報サイトへ。)


Canvasのテストとして何を作ろうかと思っているときに、iPhoneアプリQuick Graphを見つけました。


これは式を入力すると、下画像のようなグラフが簡単に描画できるというアプリです。
iPod ScreenShot-Quick Graph


このアプリには、Libraryとして幾つか図形を描画できる式が入っているのですが、これが結構面白い形で感心したので、これらをCanvasで描画してみます。
(実行ボタンを押すと動作を開始します。表示されない場合、一度ブラウザを更新してみてください。)

そもそもCanvasに対応していないブラウザもあるようなので、環境によっては上のサンプルは動作しないかもしれません。とりあえず、FirefoxIEでは動作を確認しました。
(動作が重過ぎる場合、パスの描画をOFFにするとマシになると思います。)


本来ならば(当然と言うべきか)IECanvasには対応していません。


では、なぜこのサンプルがIEで動作するかというと、Google謹製のJavaScriptライブラリ「ExplorerCanvas」を使用しているためです。


このExplorerCanvasCanvasの描画をエミュレートするもので、これを使用すればCanvasに対応していないIEでもCanvasで描画させることができるのです。素晴らしい!さすがGoogle大先生!


しかし、IEJavaScriptの動作自体が遅いので、エミュレートさせたCanvasの描画はとても遅くなっています。
(追記:コメント欄でご指摘いただきました。JavaScriptの動作が遅いせいと言う以上に、描画にVMLを用いているから速度がでないのだそうです。)


上のサンプルでも5fps前後しかでないようです。
(また、今のところCanvasの動作を完全にエミュレートできるわけではなく、一部正しく動作しないコードもあるようです。)


まだちらっとしか触ってないCanvasですが、使いやすい感じですね。
JavaScriptでのゲーム製作がよりやりやすくなったんじゃないかと思います。


一応、今回のサンプルのコードを以下においておきます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta http-equiv="Content-Language" content="ja" />
        <meta http-equiv="Content-Style-Type" content="text/css" />
        <meta http-equiv="Content-Script-Type" content="text/javascript" />
        <title>Quick Graph Test</title>
        <!--[if IE]><script type="text/javascript" src="excanvas.compiled.js"></script><![endif]-->
        <script type="text/javascript">

        // 画面サイズ
        var CWIDTH = 400;
        var CHEIGHT = 400;
        
        function Movable() {
            this.initialize.apply(this, arguments);
        }
        Movable.prototype = {
            initialize: function(x,y) {
                this.x = x;
                this.y = y;
                this.theta = 0.0;
            },
            move: function() {
                var pos = this.calcPos(this.theta);
                this.x = pos.x;
                this.y = pos.y;
                if((this.theta += 0.015) > Math.PI * 2)
                    this.theta = 0;
            },
            calcPos: function(theta) {
                return { x:0, y:0 };
            },
            draw: function(ctx) {
                ctx.beginPath();
                ctx.fillStyle = 'rgb(255, 0, 0)';
                ctx.arc(this.x, this.y, Movable.RADIUS, 0, Math.PI*2, false);
                ctx.fill();
            },
            drawPath: function(ctx) {
                var pos = this.calcPos(0);
                ctx.moveTo(pos.x, pos.y);
                for(var i = 0; i < Math.PI * 2; i += 0.01){
                    pos = this.calcPos(i);
                    ctx.lineTo(pos.x, pos.y);
                }
                ctx.stroke();
            }

        };
        Movable.RADIUS = 10;
        Movable.BASE_X = (CWIDTH * 0.5);
        Movable.BASE_Y = (CHEIGHT * 0.5);
        
        function Action1() {};
        Action1.prototype = new Movable();
        Action1.prototype.getFunction = function() {
            return "r = 3 - 6・sin(3・θ)";
        }
        Action1.prototype.calcPos = function(theta) {
            var r = 3 - 6 * Math.sin(3 * theta);
            return {
                x:Movable.BASE_X + Math.cos(theta) *(r * 20),
                y:Movable.BASE_Y + Math.sin(theta) * (r * 20)
            };
        }
        function Action2() {};
        Action2.prototype = new Movable();
        Action2.prototype.getFunction = function() {
            return "r^2 = sin(3・θ)^2+cos(0.8-r)";
        }
        Action2.prototype.calcPos = function(theta) {
            var r = Math.pow(Math.sin(3 * theta),2) + Math.cos(0.8-50);
            return {
                x:Movable.BASE_X + Math.cos(theta) * (r * 50),
                y:Movable.BASE_Y + Math.sin(theta) * (r * 50)
            };
        }
        
        function Action3() {};
        Action3.prototype = new Movable();
        Action3.prototype.getFunction = function() {
            return "r = (1-cos(8・θ))・(1+sin(θ))";
        }
        Action3.prototype.calcPos = function(theta) {
            var r = (1 - Math.cos(8 * theta) * (1 + Math.sin(theta)));
            return {
                x:Movable.BASE_X + Math.cos(theta) *(r * 50),
                y:Movable.BASE_Y + Math.sin(theta) * (r * 50)
            };
        }
        
        function Action4() {};
        Action4.prototype = new Movable();
        Action4.prototype.getFunction = function() {
            return "r = 4-4・sin(θ)";
        }
        Action4.prototype.calcPos = function(theta) {
            var r = 4 - 4 * Math.sin(theta);
            return {
                x:Movable.BASE_X + Math.cos(theta) *(r * 20),
                y:Movable.BASE_Y + Math.sin(theta) * (r * 20)
            };
        }

        var Canvas = null;
        var CanvasContext = null;
        
        var fpsCount = 0;
        var fps = 0;
        var movable = null;
        var drawPath = true;
        
        onload = function() {
            // canvas要素のノードオブジェクト
            var Canvas = document.getElementById('CanvasTest');
            // canvas要素の存在チェックとCanvas未対応ブラウザの対処
            if ( ! Canvas || ! Canvas.getContext ) {
                alert("お使いのブラウザはCanvasに対応していません。");
                return false;
            }
            // 2Dコンテキストの取得
            CanvasContext = Canvas.getContext('2d');
            
            // ループスタート
            setInterval("mainLoop()", 1000/30);
            
            // FPS
            setInterval(function(){
                document.getElementById("fpsText").innerHTML = fpsCount + " fps";
                fpsCount = 0;
            }, 1000);
            
            // Movable生成
            onChange(document.Form.selectBox);
        };
        
        function clear(ctx) {
            ctx.beginPath();
            ctx.fillStyle = 'rgb(255, 255, 255)';
            ctx.fillRect(0, 0, CWIDTH, CHEIGHT);    
        }
        
        function drawGraphBG(ctx) {
            ctx.moveTo(CWIDTH * 0.5, 0);
            ctx.lineTo(CWIDTH * 0.5, CHEIGHT);
            ctx.moveTo(0, CHEIGHT * 0.5);
            ctx.lineTo(CWIDTH, CHEIGHT * 0.5);
            ctx.stroke();
            ctx.beginPath();
            ctx.strokeRect(0, 0, CWIDTH, CHEIGHT);
        }
        
        function draw(ctx) {
            if(drawPath)
                movable.drawPath(ctx);
            movable.draw(ctx);
        }
        
        function move() {
            movable.move();
        }
        
        function mainLoop() {
            clear(CanvasContext);
            move();
            drawGraphBG(CanvasContext);
            draw(CanvasContext);
            fpsCount++;
        }
        
        // チェックボックスクリック
        function onCheck(checkBox) {
            drawPath = checkBox.checked;
        }
        // セレクトボックス変更
        function onChange(selectBox) {
            movable = eval("new " + selectBox.options[selectBox.selectedIndex].value + "(0, 0)");
        }
        </script>
    </head>
    <body>
        <canvas id="CanvasTest" width="400" height="400"></canvas>
        <div id = "fpsText" style="position:absolute;top:15px;left:15px;"></div>
        <form action="#" name="Form" style="position:absolute;top:360px;left:15px;">
            <select name="selectBox" onChange="onChange(this)">
                <option value="Action1">r = 3 - 6・sin(3・θ)</option>
                <option value="Action2">r^2 = sin(3・θ)^2+cos(0.8-r)</option>
                <option value="Action3">r = (1-cos(8・θ))・(1+sin(θ))</option>
                <option value="Action4">r = 4-4・sin(θ)</option>
            </select><br>
            <input type="checkBox" name="c1" onClick="onCheck(this)" checked>パスの描画
        </form>
    </body>
</html>