ベジエ曲線でオブジェクトを動かす


# ちょっと長い前置き
そう、まだゲーム業界の片隅に足を突っ込んだころ・・・
某絵本型カートリッジのゲームを作る会社(今は倒産してもうないけど)にいた。
そのころ、ミニゲームでイルカがボールを飛ばして的にぶつけるものを作っていた。
高校のころ、シューティングゲームでcosとsinを駆使していたからきっと三角関数使えばできる！
という今思えばわけわかんない自身に満ち溢れていた・・・
まぁ、三角関数だと落ちる場所がずれて全然ダメで悩んでいると、上司が「ベジエ関数使え」と。
そう、ライブラリにベジエ関数なるものが！
使ってみると便利で超感動したものです。
そして、あれから20年近くたった。
気晴らしにググったら、同じようなことできるもの発見！
実は過去何度か探したのに見つからなくて超うれしかった。

# やってみた
というわけで以下のサイトをもとに、ベジエ曲線でオブジェクトを動かしてみた。
[[JavaScript] 3次ベジェ曲線をCanvasに引いてみたい | CreateJS, EaselJS](http://www.kuma-de.com/blog/2015-12-22/7065)

計算部分はコピペ。
[実際に動かしたもの](https://taka-hama.sakura.ne.jp/lab/bezire.html)
ソースはJavaScriptの所だけ。全体見たい場合は↑のソース見てください。

```javascript

        var clipInfo = [];
        var initalize = true;
        var scale = 1;
        var outX = 0;
        var outY = 0;
        var target="3ds";
        var bzcnt = 0;
        var bzAdd = 0.01;

        var sizes={
             "gb" :{"width":160, "height":144}
            ,"gba":{"width":240, "height":160}
            ,"ds" :{"width":256, "height":192}
            ,"3ds":{"width":400, "height":240}
            ,"psp":{"width":480, "height":272}
            ,"psv":{"width":960, "height":544}
        };

        window.onload = function(){
            resize();
            animation();
        };
        
        window.onresize = function(){
            resize();
        }

        function addClipInfo(x, y, width, height){
            var idx = clipInfo.length;
            clipInfo[idx] = {"x":x,"y":y,"w":width,"h":height};
        }
        function resetClipInfo(){
            clipInfo = [];
        }

        function resize(){
            var frontBuffer = document.getElementById('frontbuffer');
            frontBuffer.width = document.body.clientWidth;
            frontBuffer.height = document.body.clientHeight;
            initalize = true;
        }

        function animation(){
            var startTime = new Date();
            var endTime = new Date();
            // 領域のサイズ
            var backBuffer = document.getElementById('backbuffer');
            var frontBuffer = document.getElementById('frontbuffer');
            if(initalize){
                backBuffer.width = sizes[target].width;
                backBuffer.height = sizes[target].height;
            }
            var bgw = backBuffer.width;
            var bgh = backBuffer.height;
            var w = frontBuffer.width;
            var h = frontBuffer.height;

            try{
                
                if(!backBuffer || !backBuffer.getContext){
                    return;
                }

                var ctx = backBuffer.getContext('2d');
                setSmooth(ctx,false);

                if(initalize){
                    initalize = false;
                    ctx.beginPath();
                    ctx.fillStyle = 'rgba(200, 200, 200, 1)';
                    ctx.fillRect(0, 0, bgw, bgh);
                    ctx.fill();
                }

                // クリア
                ctx.save();
                // 前の描画情報からclipする
                ctx.beginPath();
                for(i = 0; i < clipInfo.length; i++){
                        ctx.rect(clipInfo[i].x, clipInfo[i].y, clipInfo[i].w, clipInfo[i].h)
                }
                ctx.closePath();
                ctx.clip();
                ctx.fillStyle = 'rgba(200,200,200,1)';
                ctx.fill() ;
                ctx.restore();
                resetClipInfo();

                // ----------------------------------
                // 描画
                // ----------------------------------
                drawCubicBezier(ctx, {x:10,y:200}, {x:10,y:10}, {x:200,y:10}, {x:200,y:200});
                var bzpos = calcCubicBezierPoint({x:10,y:200}, {x:10,y:10}, {x:200,y:10}, {x:200,y:200}, bzcnt);
                ctx.beginPath();
                ctx.arc( Math.round(bzpos.x), Math.round(bzpos.y), 10, 0 * Math.PI / 180, 360 * Math.PI / 180, false ) 
                ctx.fillStyle = 'rgba(128, 128, 128)';
                ctx.fill() ;
                addClipInfo(Math.round(bzpos.x) - 10, Math.round(bzpos.y) - 10, 20, 20);

                bzcnt += bzAdd;
                if(bzcnt > 1){
                    bzcnt = 1;
                    bzAdd = -0.01;
                }else if(bzcnt < 0){
                    bzcnt = 0;
                    bzAdd = 0.01;
                }
            }catch(e){
                console.log(e);
            }finally {
                count();
                ctx.beginPath();
                ctx.shadowColor = 'rgb(0,0,0)';
                ctx.shadowOffsetX = 0;
                ctx.shadowOffsetY = 0;
                ctx.shadowBlur = 0;
                ctx.font = "12px serif";
                ctx.fillStyle = 'rgba(128,128,128,1)';
                ctx.fillText("FPS : " + Math.round(framerate * 100) / 100, 24, 24);
                ctx.font = "12px serif";
                ctx.fillStyle = 'rgba(128,128,128,1)';

                addClipInfo(0, 0, 200, 48);

                var f_ctx = frontBuffer.getContext('2d');
                setSmooth(f_ctx,false);

                if(w > h){
                    // 高さに合わせる
                    scale = Math.floor((h / bgh) * 10) / 10;
                }else{
                    // 幅に合わせる
                    scale = Math.floor((w / bgw) * 10)/ 10;
                }
                outX = Math.floor((w - bgw * scale) / 2);
                outY = Math.floor((h - bgh * scale) / 2);

                f_ctx.drawImage( backBuffer, outX, outY, bgw * scale, bgh * scale );
                window.requestAnimationFrame (animation);
            }
        }

        function setSmooth(ctx, flag){
            ctx.imageSmoothingEnabled = flag;
            ctx.mozImageSmoothingEnabled = flag;
            ctx.webkitImageSmoothingEnabled = flag;
        }

        var fpscount = 0;
        var basetime = new Date();
        var framerate = 0;
        function count(){
            fpscount++;
            var now = new Date();
            if(now - basetime >= 1000){
                framerate = (fpscount * 1000) / (now - basetime);
                fpscount = 0;
                basetime = new Date();
            }
        }


        //http://hakuhin.jp/as/curve.html#CURVE_02
        function calcCubicBezierPoint(p0, p1, p2, p3, t){
            var resultPoint = {x:0, y:0};

            var t2 = 1 - t;
            var v = t2 * t2 * t2;
            resultPoint.x += v * p0.x;
            resultPoint.y += v * p0.y;

            v = 3 * t2 * t2 * t;
            resultPoint.x += v * p1.x;
            resultPoint.y += v * p1.y;

            v = 3 * t * t * t2;
            resultPoint.x += v * p2.x;
            resultPoint.y += v * p2.y;

            v = t * t * t;
            resultPoint.x += v * p3.x;
            resultPoint.y += v * p3.y;

            return resultPoint;
        }

        // http://www.kuma-de.com/blog/2015-12-22/7065
        function drawCubicBezier(context, startPoint, anchorPoint1, anchorPoint2, endPoint){
            var devideNum = 40.0; //分割数 多いほどなめらか
            var dt = 1.0 / devideNum;
            var drawPoint;
            context.beginPath();
            context.fillStyle = 'rgba(0, 0, 0, 1)';
            var t;
            for(t = 0; t < 1; t = t + dt){
                drawPoint = calcCubicBezierPoint(startPoint, anchorPoint1, anchorPoint2, endPoint, t);
                var dx = Math.round(drawPoint.x);
                var dy = Math.round(drawPoint.y);
                if(t == 0){
                    context.moveTo(dx, dy);
                }else{
                    context.lineTo(dx, dy);
                }
            }
            drawPoint = calcCubicBezierPoint(startPoint, anchorPoint1, anchorPoint2, endPoint, 1);
            var dx = Math.round(drawPoint.x);
            var dy = Math.round(drawPoint.y);
            context.lineTo(dx, dy);
            context.stroke();
        }
```

ちなみに、ゲーム用に実験してるHTMLでやっちゃったのでいらないもんいっぱい描いてあるけど気にしないでね！^^;
さきに余計なほうを説明すると、これバックバッファを更新してフロントバッファに転送しています。
で、バックバッファを更新するときに、クリッピングしてます。
(今思ったけど、フロントバッファでやったほうがいいのかな・・・？)
で、ゲームボーイサイズでバックバッファを作って、フロントバッファに拡縮して転送しています。
レトロっぽいもの作りたいんです！

ベジエ曲を描いているのは、ほぼパクリ(若干直したけど)のdrawCubicBezier()メソッド。
で、肝心のベジエで動かしているところは、もろコピペした(苦笑)calcCubicBezierPoint()メソッド呼び出している以下の所。

```javascript
var bzpos = calcCubicBezierPoint({x:10,y:200}, {x:10,y:10}, {x:200,y:10}, {x:200,y:200}, bzcnt);
ctx.beginPath();
ctx.arc( Math.round(bzpos.x), Math.round(bzpos.y), 10, 0 * Math.PI / 180, 360 * Math.PI / 180, false ) 
ctx.fillStyle = 'rgba(128, 128, 128)';
ctx.fill() ;
```

計算の内容は全く理解していないので、どこかでしっかり学ぶとして・・・
calcCubicBezierPointメソッドは、開始位置、終了位置、
bzcntが0<=bzcnt<=1の値が入ります。
この数値が細かいほど滑らかに動きますが、計算量が増えるので動きは遅くなります。

また、今回の場合だと、計算後の座標をroundして丸めています。
これは単にクリッピングでゴミが残ってしまったのでその対策。

2Dのゲーム作るときに使えるかもしれないので、知っておいて損はないかも。