JavaScript
16. 描画の軌跡を考える
「13. 描画の単位を考える」「14. ベジエ曲線を利用する」の項目では描画範囲を縦横に並べています。横に並べる、縦に並べるというのはx座標、y座標の値どちらか一方を変化させれば良いので比較的簡単です。
ここでは斜めに配置する方法について考えてみます。
斜めの軌跡に添い描画単位を配置する1
window.addEventListener( "load", loadFunc, false );
function loadFunc() {
var canvas = document.getElementById( "stage" );
var ctx = canvas.getContext( "2d" );
var shapeSize = 40;
var firstPointX = 1;
var firstPointY = 1;
var plusX = 20;
var plusY = 40;
var boundsX = canvas.width + shapeSize;
var boundsY = canvas.height + shapeSize;
ctx.strokeStyle = "hsla( 150, 50%, 50%, 1 )";
ctx.fillStyle = "hsla( 150, 50%, 50%, 0.5 )";
var drawPointX = firstPointX;
var drawPointY = firstPointY;
while( drawPointX < boundsX && drawPointY < boundsY ) {
drawShape( ctx, drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
}
drawPointX = firstPointX;
drawPointY = firstPointY;
while( drawPointX < boundsX && drawPointY < boundsY ) {
drawShape( ctx, drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
}
function drawShape( argCtx, centerX, centerY, size ) {
argCtx.beginPath();
argCtx.arc( centerX, centerY, size, 0, Math.PI*2, false );
argCtx.stroke();
}
簡易に行うのであれば、繰返し描画を行う中で、x座標の値とy座標の増分を異なる数値にすれば、斜めに描画単位を配置することができます。
24、32行目、whileの後の()の中の条件の書き方ですが、&&などでつなげて、条件を2つ書くことができます。drawPointX < boundsX && drawPointY < boundsYとは、drawPointXもdrawPointYも描画範囲の最大値以下(drawPointXもcanvas.width+shapeSize以下、drawPointYもcanvas.height+shapeSize以下)の場合に繰返しが続きます。

描画単位に応じて、この方法で要件を満たすのであれば、この簡易的な方法でも良いでしょう。
function drawShape( argCtx, centerX, centerY, size ) {
var tmpSize = Math.floor( Math.random() * size/4 ) + size/4;
var tmpX = Math.floor( Math.random() * tmpSize ) - tmpSize/2;
var tmpY = Math.floor( Math.random() * tmpSize ) - tmpSize/2;
argCtx.beginPath();
argCtx.arc( centerX + tmpX, centerY + tmpY, tmpSize, 0, Math.PI*2, false );
argCtx.fill();
var drawRepeat = 4;
for( var i = 0; i < drawRepeat; i++ ) {
tmpSize = Math.floor( Math.random() * size/8 ) + size/8;
tmpX = Math.floor( Math.random() * size/2 ) - size/4;
tmpY = Math.floor( Math.random() * size/2 ) - size/4;
argCtx.beginPath();
argCtx.arc( centerX + tmpX, centerY + tmpY, tmpSize, 0, Math.PI*2, false );
argCtx.fill();
}
}

drawShape()の内容だけ、「13. 描画の単位を考える」で試みた内容に置き換えています。
斜めに配置するこの簡易的な方法の問題点は、「x座標とy座標の値を繰返しの中で増やすので、ある点からある点までの直線に添わせる、という書き方が難しい」、「斜線上の距離をもとに描画単位のx座標とy座標を決めることができない」という点にあります。
例えば上記の描画ですが、x座標の増分が同じで、描画単位同士の間隔は異なってしまっています。
それらの問題は三角関数を利用して座標を求めることで解決されます。
斜めの軌跡に添い描画単位を配置する2
ここでは多少複雑ですが、2つの点を結ぶような直線を仮想して、それに添って、描画単位を配置することを考えてみます。
詳細は割愛しますが、点1と点2を結ぶ直線の傾きは、以下のように求めることができます。
angleが角度(ラジアン)、firstPointX:点1のx座標、firstPointY:点1のy座標、secondPointX:点2のx座標、secondPointY:点2のy座標です。
angle = Math.atan2( secondPointY - firstPointY, secondPointX - firstPointX )
そして、その直線上で例えば40の距離を取りたいとします。その場合の水平の距離(plusX)と垂直の距離(plusY)は、次のようになります。
plusX = Math.cos( angle ) * 40
plusY = Math.sin( angle ) * 40
したがって、以下のように書けば、ある点からある点までの距離を指定して、描画単位を描画することができます。
window.addEventListener( "load", loadFunc, false );
function loadFunc() {
var canvas = document.getElementById( "stage" );
var ctx = canvas.getContext( "2d" );
var shapeSize = 20;
var firstPointX = 1; //点1のx座標
var firstPointY = 1; //点1のy座標
var secondPointX = 500; //点2のx座標
var secondPointY = 125; //点2のy座標
var thirdPointX = 1; //点3のx座標
var thirdPointY = 500; //点3のy座標
var pitch = 40; //描画単位を描く間隔
var angle = 0; //角度
var plusX = 0; //x座標の増分
var plusY = 0; //y座標の増分
var boundsX = canvas.width + shapeSize;
var boundsY = canvas.height + shapeSize;
ctx.strokeStyle = "hsla( 150, 50%, 50%, 1 )";
ctx.fillStyle = "hsla( 150, 50%, 50%, 0.5 )";
//描画位置を点1にする
var drawPointX = firstPointX;
var drawPointY = firstPointY;
//点1と点2を結ぶ直線の角度を求める
angle = Math.atan2( secondPointY - firstPointY, secondPointX - firstPointX );
//その直線上の間隔がpitchの場合のxとyを求める
plusX = Math.cos( angle ) * pitch;
plusY = Math.sin( angle ) * pitch;
//drawPointXが-shapeSizeより大きくboundsX以下で、drawPointYが-shapeSizeより大きくboundsY以下の場合に繰り返す。
while( -shapeSize < drawPointX && drawPointX < boundsX && -shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( ctx, drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
//描画位置を点2にする
var drawPointX = secondPointX;
var drawPointY = secondPointY;
//点2と点3を結ぶ直線の角度を求める
angle = Math.atan2( thirdPointY - secondPointY, thirdPointX - secondPointX );
//その直線上の間隔がpitchの場合のxとyを求める
plusX = Math.cos( angle ) * pitch;
plusY = Math.sin( angle ) * pitch;
//drawPointXが-shapeSizeより大きくboundsX以下で、drawPointYが-shapeSizeより大きくboundsY以下の場合に繰り返す。
while( -shapeSize < drawPointX && drawPointX < boundsX && -shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( ctx, drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
//補助のため、直線を描画
ctx.strokeStyle = "hsla( 150, 50%, 0%, 0.5 )";
ctx.beginPath();
ctx.moveTo( firstPointX, firstPointY );
ctx.lineTo( secondPointX, secondPointY );
ctx.lineTo( thirdPointX, thirdPointY );
ctx.stroke();
}
function drawShape( argCtx, centerX, centerY, size ) {
argCtx.beginPath();
argCtx.arc( centerX, centerY, size, 0, Math.PI*2, false );
argCtx.stroke();
}
31〜47行目、1つ目の軌跡に添って描画しています。点1( 0, 0 )から点2(500, 125)まで間隔40で円が描かれます。
49〜65行目、2つ目の軌跡に添って描画しています。点2(500, 125)から点3( 0, 500 )まで間隔40で円が描かれます。
なお43、61行目、whileの条件は4つに増やしています。

曲線の傾きが変わっても、円の間隔は等しく描画されています。
座標を配列に格納する
次々と線を描いていったり、描画のための数値がいくつかに決まっているような場合には、変数よりも配列を使った方が便利な場合があります。
window.addEventListener( "load", loadFunc, false );
function loadFunc() {
var canvas = document.getElementById( "stage" );
var ctx = canvas.getContext( "2d" );
var shapeSize = 20;
var pointNum = 5;
//座標を格納するための配列を作る
var pointArray = new Array();
//配列にx座標とy座標を格納するため、配列にさらに配列を代入する(多次元配列にする)
for( var i = 0; i < pointNum; i++ ) {
pointArray[i] = new Array();
}
pointArray[0][0] = 1; //点1のx座標
pointArray[0][1] = 1; //点1のy座標
pointArray[1][0] = 500; //点2のx座標
pointArray[1][1] = 75; //点2のy座標
pointArray[2][0]= 1; //点3のx座標
pointArray[2][1] = 150; //点3のy座標
pointArray[3][0]= 500; //点4のx座標
pointArray[3][1] = 270; //点4のy座標
pointArray[4][0]= 1; //点5のx座標
pointArray[4][1] = 500; //点5のy座標
var pitch = 40; //描画単位を描く間隔
var angle = 0; //角度
var plusX = 0; //x座標の増分
var plusY = 0; //y座標の増分
var boundsX = canvas.width + shapeSize;
var boundsY = canvas.height + shapeSize;
ctx.strokeStyle = "hsla( 150, 50%, 50%, 1 )";
ctx.fillStyle = "hsla( 150, 50%, 50%, 0.5 )";
for( var i = 0; i < pointNum-1; i++ ) {
//描画位置を配列から取得
var drawPointX = pointArray[i][0];
var drawPointY = pointArray[i][1];
//現在の点と次の点を結ぶ直線の角度を求める
angle = Math.atan2( pointArray[i+1][1] - pointArray[i][1], pointArray[i+1][0] - pointArray[i][0] );
//その直線上の間隔がshapePitchの場合のxとyを求める
plusX = Math.cos( angle ) * pitch;
plusY = Math.sin( angle ) * pitch;
//描画範囲画面より少し大きい間だけ描画する
while( -shapeSize < drawPointX && drawPointX < boundsX && -shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( ctx, drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
}
//補助のため、直線を描画
ctx.strokeStyle = "hsla( 150, 0%, 75%, 1 )";
ctx.beginPath();
ctx.moveTo( pointArray[0][0], pointArray[0][1] );
for( var i = 1; i < pointNum; i++ ) {
ctx.lineTo( pointArray[i][0], pointArray[i][1] );
ctx.stroke();
}
}
function drawShape( argCtx, centerX, centerY, size ) {
argCtx.beginPath();
argCtx.arc( centerX, centerY, size, 0, Math.PI*2, false );
argCtx.stroke();
}
配列は多次元にすることもできます。
ここでいう多次元とは、例えばpointArray[0]にも、pointArray[1]にも、それぞれ複数の値を格納できるようにすることです。
上記であれば、x座標とy座標を格納するとき、pointArray[0]にx座標、pointArray[1]にy座標…などと格納するよりも、pointArray[0][0]にx座標、pointArray[0][1]にy座標…と格納できた方が、始めの数値が何番目の点かを示し、2つめの数値が0であればx座標、1であればy座標となるので理解、管理がしやすくなります。
13行目、var pointArray = new Array();でpointArrayという配列を作っています。
そして、16〜18行目のfor文で、pointArray[0]〜pointArray[4]までのそれぞれにまた配列を作っています。
このようにすると、47〜67行目のように、for文で繰返し座標を指定しての描画が行えるので便利です。

描画単位を置き換えたとしても、中心座標から軌跡の距離は等しくなるので、同じ速さの筆の動きで描写したようになります。
window.addEventListener( "load", loadFunc, false );
function loadFunc() {
var canvas = document.getElementById( "stage" );
var ctx = canvas.getContext( "2d" );
var shapeSize = 40;
var pointNum = 5;
//座標を格納するための配列を作る
var pointArray = new Array();
//配列にx座標とy座標を格納するため、配列にさらに配列を代入する(多次元配列にする)
for( var i = 0; i < pointNum; i++ ) {
pointArray[i] = new Array();
pointArray[i] = new Array();
}
pointArray[0][0] = 1; //点1のx座標
pointArray[0][1] = 1; //点1のy座標
pointArray[1][0] = 500; //点2のx座標
pointArray[1][1] = 75; //点2のy座標
pointArray[2][0]= 1; //点3のx座標
pointArray[2][1] = 150; //点3のy座標
pointArray[3][0]= 500; //点4のx座標
pointArray[3][1] = 270; //点4のy座標
pointArray[4][0]= 1; //点5のx座標
pointArray[4][1] = 500; //点5のy座標
var pitch = 40; //描画単位を描く間隔
var angle = 0; //角度
var plusX = 0; //x座標の増分
var plusY = 0; //y座標の増分
var boundsX = canvas.width + shapeSize;
var boundsY = canvas.height + shapeSize;
ctx.strokeStyle = "hsla( 150, 50%, 50%, 1 )";
ctx.fillStyle = "hsla( 150, 50%, 50%, 0.5 )";
for( var i = 0; i < pointNum-1; i++ ) {
drawPointX = pointArray[i][0];
drawPointY = pointArray[i][1];
angle = Math.atan2( pointArray[i+1][1] - pointArray[i][1], pointArray[i+1][0] - pointArray[i][0] );
plusX = Math.cos( angle ) * pitch;
plusY = Math.sin( angle ) * pitch;
while( -shapeSize < drawPointX && drawPointX < boundsX && -shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( ctx, drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
}
}
function drawShape( argCtx, centerX, centerY, size ) {
var tmpSize = Math.floor( Math.random() * size/4 ) + size/4;
var tmpX = Math.floor( Math.random() * tmpSize ) - tmpSize/2;
var tmpY = Math.floor( Math.random() * tmpSize ) - tmpSize/2;
argCtx.beginPath();
argCtx.arc( centerX + tmpX, centerY + tmpY, tmpSize, 0, Math.PI*2, false );
argCtx.fill();
var drawRepeat = 4;
for( var i = 0; i < drawRepeat; i++ ) {
tmpSize = Math.floor( Math.random() * size/8 ) + size/8;
tmpX = Math.floor( Math.random() * size/2 ) - size/4;
tmpY = Math.floor( Math.random() * size/2 ) - size/4;
argCtx.beginPath();
argCtx.arc( centerX + tmpX, centerY + tmpY, tmpSize, 0, Math.PI*2, false );
argCtx.fill();
}
}

ここまでくると、描画単位とは例えばとある種類の画材(筆など)の特徴であり、それを軌跡にそって描画することはその画材で線を描くことに相当することが理解されるかと思います。
この方法に限りませんが、「描画単位+軌跡」の組み合わせを独自にどのように考えるかが、プログラムで行うヴィジュアル生成の1つの作り方になります。