Canvas画像処理

【JavaScript】 Canvasの原点を左下にしてグラフを描画

更新日:2021/08/31

JavaScriptのCanvasAPIを使用すると、ブラウザに図形を描画できます。

しかし左下を原点としてグラフを描画しようとすると、少し手間取ることがあります。

どうすればいいのでしょうか。

 

Canvasの座標系とグラフ

Canvasは左上を原点として、右側に向かってx値が増加します。
問題はy値です。
こちらは、下側に向かって増加します。

canvasの座標系

一方、一般的なグラフは上方向に向かってy値が増加します。

一般的なグラフの座標系

外部から受け取ったデータでグラフを描画するケースが考えられますが、素直に描画してしまうと次のように反転することになります。

素直に描画するとグラフが反転する

そのため、位置を補正しながら描画する必要があります。

 

描画位置の補正の概念

左下を原点として、上方向に向かってy値を増加させるのは、それほど難しくありません。

次のようなケースを考えてみます。

原点を( 10 , 100 )に移動

  1. 原点を( 10 , 100 )に移動
  2. 上方向に向かってY値を増加

x値は、描画時に10を加算するだけです。
つまり、次のような式になります。

x' = x + 10

y値は、描画時に100を減算して絶対値を求めます。
つまり、次のような式になります。

y' = Math.abs( y - 100 )

例えばy値が10のときは、計算結果が90です。
y値が20のときは、計算結果80です。
合ってます。

問題はy値にマイナスを指定したときです。
y値が-10のときは、計算結果が110です。
y値が-20のときは、計算結果が120です。

これもあってますね。

これをもとに、直線を描画するコードを作成してみます。


const canvas =document.createElement("canvas");
const {width,height} = canvas;
const context = canvas.getContext("2d"); 

const originX = 10;
const originY = 100;

const transPoint = ( x , y ) => [ x + originX , Math.abs( y - originY ) ];
context.fillStyle = "black";
context.beginPath();
context.moveTo( ...transPoint(originX * -1 , 0) );
context.lineTo( ...transPoint(width - originX  , 0 ));
context.stroke();

moveToとlineToの引数で使っている「...」は、スプレッド構文です。
transPointから受け取った要素を二つ持つ配列を、2つの引数に展開しています。

スプレッド構文については次のページを読んでみてください。
【JavaScript】 コード中の「...」は意味があった スプレッド/レスト構文

 

もっとも簡単な補正方法

上で紹介した方法は、座標指定のたびに関数を呼び出す必要があり、かなりの手間となります。

実は次の二つのメソッドを実行するだけで、以降はグラフ座標での指定が可能になります。

context.translate( 原点X , 原点Y );
context.scale(1, -1);

原点X と原点Yは、グラフの原点としたいキャンバス座標です。

この二つのメソッドは、座標変換方法を登録します。
その後座標が指定されると、登録された情報をもとに、座標が変換されます。

translateは指定した値を、座標値に加算します。
つまり( 0 , 0 )は、( 原点X , 原点Y )に変換されます。

scaleは拡大率を指定するメソッドですが、-1を指定するとy軸を中心として反転してくれます。
1は、何もしないという意味です。

つまり( 100 , 100 )は、( 100 + 原点X , 100 + 原点Y )に移動した後、原点Yを起点とした水平軸を中心に反転します。

これで意図した位置に、グラフを描画できます。

 

グラフ作成例

簡単な折れ線グラフと棒グラフを描画してみます。

下のコードを実行すると、次のようなグラフが描画されます。

グラフ作成例

HTML


<canvas id="canvas" width="300" height="300"></canvas>

JavaScript


"use strict";
window.addEventListener( "DOMContentLoaded" , ()=> {

    const line = (context,sx,sy,ex,ey,width,color) => {
        context.save();
        context.beginPath();
        context.lineWidth = width;
        context.strokeStyle = color;
        context.moveTo( sx,sy );
        context.lineTo( ex,ey );
        context.stroke();
        context.restore();
    };
    const circle = (context,cx,cy,radius,lineColor,fillColor) => {
        context.beginPath();
        context.save();
        context.strokeStyle = lineColor;
        context.fillStyle = fillColor;
        context.arc( cx,cy, radius, 0, 2 * Math.PI ,false);
        context.fill();
        context.stroke();
        context.restore();
    };
    const rect = (context,sx,sy,ex,ey,color) => {

        context.save();
        context.fillStyle = color;
        context.fillRect(
            Math.min( sx , ex ) , Math.min( sy , ey ),
            Math.abs( sx - ex ) , Math.abs( sy - ey ));
        context.restore();
    };

    const initialCanvas = id => {

        const originX = 10; // 原点位置(Canvas左下
        const originY = 10; // からの距離)

        const canvas =document.getElementById(id);
        const {width,height} = canvas;

        const context = canvas.getContext("2d");

        context.translate( originX, height - originY );
        context.scale(1, -1);
             // x,y軸を描画
        line( context , originX * -1 , 0 , width - originX  , 0 , 1 , "black" );
        line( context , 0 , originY * -1 , 0 , height - originY  ,  1 , "black" );

        return context;
    };

    const drawGraph = (  context , graphData ) =>{
        const graphStep = 40;
        const rectWidth = 20 / 2;

        // 棒グラフを描画
        graphData.forEach( (posY,index) => {
            const posX = ( index + 1 ) * graphStep;

            rect( context , posX - rectWidth , posY , posX + rectWidth , 0 , "pink" );
        })

        // 折れ線グラフを描画
        let start = [  graphStep  , graphData[ 0 ] ];

        for( let i = 1 ; i < graphData.length ; i ++ ){
            const end = [ ( i + 1 ) * graphStep , graphData[ i ] ];
            line( context , ...start , ...end , 3 , "blue" );
            circle( context,...start,5,"blue","white");
            if( i === graphData.length -1 )
                circle( context,...end,5,"blue","white");
            start = end;
        }

    };

    const context = initialCanvas( "canvas" );

    drawGraph( context , [ 120 , 170 , 130 , 125 , 190 , 155 ] );

});

 

グラフ座標でのキャンバスのクリア

キャンバスの描画内容をクリアするとき、clearRectメソッドを使用します。

clearRectの形式

コンテキスト.clearRect( 左上x座標, 左上y座標 , クリアする幅 , クリアする高さ );

通常なら、左上を( 0, 0 )として、キャンバスの幅と高さを与えるだけです。

しかしこのメソッドも、変換後の座標が適用されます。

そのためキャンバス全体をクリアするには、変換後の左上座標を計算する必要があります。

次のようなコードで、座標系を変更したときのケースを考えてみます。

context.translate( x , y );
context.scale( 1, -1 );

このとき、次のようなコードでキャンバス全体をクリアできます。

const [ width , height ] = キャンバス;
const left = -1 * x;
const top = y;
const clearWidth = width;
const clearHeight = -1 * height;

コンテキスト.clearRect( left, top , clearWidth , clearHeight );

もし、複雑な変換をおこなっている場合は、次のように座標変換をリセットしてから、クリアするのが手っ取り早いです。

コンテキスト.setTransform( 1,0,0,1,0,0 );
コンテキスト.clearRect( 左上x座標, 左上y座標 , クリアする幅 , クリアする高さ );

この後、もう一度、座標変換のコードを実行してください。

更新日:2021/08/31

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。