HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う

第3回Canvasの四辺をランダムなイージングで移動する

前回までのあらすじは、Canvasの左右をランダムな垂直位置と時間で行き来するトゥイーンができたところだった。今回は、いよいよお題の動きを仕上げる。ランダムなトゥイーンに、さらにふたつの要素を加えたい。ひとつは、イージングも複数の中からランダムに選びたい。もうひとつは、トゥイーンの行き先を左右の端だけでなく、Canvasの四辺に拡げる。そして、そのときランダムの偏りについて少しばかりこだわる。

イージングをランダムに変える

まず、前回つくった第2回コード2をおさらいとして掲げておく。ランダムなトゥイーンの設定を関数(setRandomTween())に定め、トゥイーンの終わりにTween.call()メソッドで呼出すことにより、新たなトゥイーンを始めている。ランダムに定めるのは、トゥイーンにかける時間と行き先の垂直座標だ。トゥイーンを始める端は左右交互に入れ替わるので、変数(currentSide)に覚えさせた。

第2回 コード2 ランダムな垂直位置と時間で左右を行き来するトゥイーンアニメーション(再掲)
var stage;
var myBitmap;
var top = 0;
var bottom;
var left = 0;
var right;
var currentSide;
function initialize() {
  canvasObject = document.getElementById("myCanvas");
  var file = "images/Pen.png";
  var loader = new createjs.LoadQueue(false);
  right = canvasObject.width;
  bottom = canvasObject.height;
  stage = new createjs.Stage(canvasObject);
  loader.addEventListener("fileload", draw);
  loader.loadFile(file);
}
function draw(eventObject) {
  var myImage = eventObject.result;
  var halfWidth = myImage.width / 2;
  var halfHeight = myImage.height / 2;
  top += halfHeight;
  bottom -= halfHeight;
  left += halfWidth;
  right -= halfWidth;
  myBitmap = new createjs.Bitmap(myImage);
  myBitmap.regX = halfWidth;
  myBitmap.regY = halfHeight;
  myBitmap.x = halfWidth;
  myBitmap.y = (top + bottom) / 2;
  stage.addChild(myBitmap);
  stage.update();
  setRandomTween(myBitmap, "left");
  createjs.Ticker.addEventListener("tick", stage);
}
function setRandomTween(target, side) {
  var nextPoint = getNextPosition(side);
  var randomTime = Math.random() * 5000 + 1000;
  setTween(target, nextPoint, randomTime, createjs.Ease.bounceOut);
}
function setTween(target, myPoint, time, easing) {
  createjs.Tween.get(target)
  .to({x:myPoint.x, y:myPoint.y}, time, easing)
  .call(setRandomTween, [target, currentSide]);
}
function getNextPosition(side) {
  var nextX;
  var nextY = Math.random() * (bottom - top) + top;
  if (side == "left") {
    currentSide = "right";
    nextX = right;
  } else {
    currentSide = "left";
    nextX = left;
  }
  return new createjs.Point(nextX, nextY);
}

ランダムに選ぶイージングは、もともと使っていたバウンドのEase.bounceOutに、滑らかな変化(circ)と弾力のある動き(elastic)を加える。イージングの与え方(In/Out/InOut)もそれぞれ変えることにして、Ease.circInEase.elasticInOutメソッドを選んだ。第1回にご紹介したTweenJSのページのデモSpark Tableで、3つのイージングの変化を比べてみたのがつぎの図1だ。

図1 バウンドと滑らかな変化および弾力のあるイージング
図1 バウンドと滑らかな変化および弾力のあるイージング

イージングの種類も数も、後で自由に変えられるようにしたい。すると、それらの参照を配列に入れて扱うのがお約束だ。そこで、つぎのように配列(easings)を定めた。すると、そのArray.lengthプロパティ値(エレメント数)未満の整数をランダムに求めて、そのインデックスのイージング(easing)を取出せばよい。

var easings = [createjs.Ease.circIn, createjs.Ease.bounceOut, createjs.Ease.elasticInOut];

function setRandomTween(target, side) {   var nextPoint = getNextPosition(side);   var randomTime = Math.random() * 5000 + 1000;   var easing = easings[Math.floor(Math.random() * easings.length)];   // setTween(target, nextPoint, randomTime, createjs.Ease.bounceOut);   setTween(target, nextPoint, randomTime, easing); }

さらに、次項でオブジェクトの動きをCanvasの四辺に拡げる。そのため、動く範囲の座標は、関数(setGeometricData())を分けて定めることにした。今のところ、やっている中身に変わりはない。

function draw(eventObject) {
  var myImage = eventObject.result;
  var halfWidth = myImage.width / 2;
  var halfHeight = myImage.height / 2;
  /*
  top += halfHeight;
  bottom -= halfHeight;
  left += halfWidth;
  right -= halfWidth;
  */
  setGeometricData(halfWidth, halfHeight);
  // ...[中略]...
}
function setGeometricData(offsetX, offsetY) {
  top += offsetY;
  bottom -= offsetY;
  left += offsetX;
  right -= offsetX;
}

第2回コード2にこれらの手を加えたのが、つぎのコード1だ。トゥイーンの時間と移動する垂直座標に加えて、イージングが前述の3つからランダムに選ばれる。イージングの配列(easings)の中身を変えれば、選択肢は自由に変えられる。

コード1 トゥイーンの時間と垂直位置およびイージングがランダムに左右を行き来するアニメーション
var stage;
var myBitmap;
var top = 0;
var bottom;
var left = 0;
var right;
var currentSide;
var easings = [createjs.Ease.circIn, createjs.Ease.bounceOut, createjs.Ease.elasticInOut];
function initialize() {
  canvasObject = document.getElementById("myCanvas");
  var file = "images/Pen.png";
  var loader = new createjs.LoadQueue(false);
  right = canvasObject.width;
  bottom = canvasObject.height;
  stage = new createjs.Stage(canvasObject);
  loader.addEventListener("fileload", draw);
  loader.loadFile(file);
}
function draw(eventObject) {
  var myImage = eventObject.result;
  var halfWidth = myImage.width / 2;
  var halfHeight = myImage.height / 2;
  setGeometricData(halfWidth, halfHeight);
  myBitmap = new createjs.Bitmap(myImage);
  myBitmap.regX = halfWidth;
  myBitmap.regY = halfHeight;
  myBitmap.x = halfWidth;
  myBitmap.y = (top + bottom) / 2;
  stage.addChild(myBitmap);
  stage.update();
  setRandomTween(myBitmap, "left");
  createjs.Ticker.addEventListener("tick", stage);
}
function setGeometricData(offsetX, offsetY) {
  top += offsetY;
  bottom -= offsetY;
  left += offsetX;
  right -= offsetX;
}
function setRandomTween(target, side) {
  var nextPoint = getNextPosition(side);
  var randomTime = Math.random() * 5000 + 1000;
  var easing = easings[Math.floor(Math.random() * easings.length)];
  setTween(target, nextPoint, randomTime, easing);
}
function setTween(target, myPoint, time, easing) {
  createjs.Tween.get(target)
  .to({x:myPoint.x, y:myPoint.y}, time, easing)
  .call(setRandomTween, [target, currentSide]);
}
function getNextPosition(side) {
  var nextX;
  var nextY = Math.random() * (bottom - top) + top;
  if (side == "left") {
    currentSide = "right";
    nextX = right;
  } else {
    currentSide = "left";
    nextX = left;
  }
  return new createjs.Point(nextX, nextY);
}

ランダムの偏りを考える

】つぎに、四辺からランダムに行き先を定めたい。すぐに思いつくのは、四辺からひとつを選び、その上でランダムな位置を決めることだ。ただ、ランダムに値を定めるときには、結果に偏りがないかはつねに気にかけたい。四辺からランダムにひとつを選んで、その後位置決めをするという考え方で試してみよう。幅200×高さ50ピクセルの矩形上に、50個の点をランダムに描いてみた図2⁠。

図2 四辺からひとつを選んでランダムに位置決めする
図2 四辺からひとつを選んでランダムに位置決めする

垂直の辺は点の間が詰まっていて、水平の辺はまばらだ。理屈を考えれば、理由はわかる。四辺から偏りなくひとつを選べば、各辺にはほぼ同じ数の点が置かれる。すると、短い辺には長い辺より、長さ当たりで比べると多くの点が集まることになる。jsdo.itにテスト用のコードを上げたので、興味のある読者は試してほしい。

この結果が、必ずしも悪いということではない。TVの日本列島ダーツの旅は広い地域が選ばれやすい。全国を見て回るという番組としてはそれでよいのだろう。けれど、選挙で選ばれる議員の数は、有権者数に応じなければならない[1]⁠。偏っていてもよいか、あるいは何を偏っていると見るかは、詰まるところ制作者が決めることだ。

今回はランダムについてこだわりたいので、Canvasの四辺から長さ当たりの偏りがないように点を選ぶことにする。どう考えるかというと、四辺を1本に延ばして直線とみる。その中でランダムな1点を選んでから、改めて四角に戻してどの辺か確かめ、座標を決めればよい。

  1. 四辺を1本の直線に伸ばす
  2. 直線の上の1点をランダムに選ぶ
  3. 直線を矩形に戻して点の座標を調べる

四辺から行き先をランダムに決める

それでは、前項に述べた考え方にしたがって、スクリプトを組立てよう。ランダムなトゥイーンを設定する関数(setRandomTween())に手を加える。この中から呼出していた行き先座標を決める関数(getNextPosition())は、1本の直線に伸ばした辺の上のランダムな1点を、xy座標(Pointオブジェクト)でなく数値で返すことにする。そして、その数値を矩形の辺上のxy座標に直す関数(getPoint())は新たに定める。

  // ...[中略]...
function setRandomTween(target, side) {
  // var nextPoint = getNextPosition(side);
  var position = getNextPosition(side);
  var nextPoint = getPoint(position);
  // ...[中略]...
}

四辺を扱うための変数も新たに加える。まず、延ばした直線から四辺に区切って調べる順番を配列(sides)に納めた。左辺("left")から時計回りとする。つぎに、各辺の長さも配列(lengths)にした。四辺の長さを合計した数値の変数(lengthsTotal)と併せて、座標の数値を求める関数(setGeometricData())の中で定めることにした。

var sides = ["left", "top", "right", "bottom"];
var lengths;
function setGeometricData(offsetX, offsetY) {
  // ...[中略]...
  var width = right - left;
  var height = bottom - top;
  lengths = [height, width, height, width];
  lengthsTotal = height + width + height + width;
}

では、1本に延ばした辺からランダムな1点を決める関数の手直しだ。先の説明では細かいところを端折ったので、それを先に補っておこう。

ひとつは、トゥイーンを始める辺は行き先から除く。つまり、残りの3辺をつなげた1本の直線からランダムな点を選ぶ。これは、同じ辺のうえをトゥイーンしたのでは、動きが小さくなるからだ。もうひとつ、返す値にはピクセルの数値だけでなく、それが四辺のどれかを示す文字列も含める。つまり、ふたつの値をプロパティ(positionとside)に納めたObjectインスタンス(returnObject)で返す。

関数(getNextPosition())は、トゥイーンを始める辺の配列(sides)におけるインデックス(index)から、その辺が除かれた3辺の合計の長さ(range)を求め、その範囲内のランダムな数値(position)を得る。そのうえで、それが3辺のどの位置なのかをforループで調べることになる。

辺を調べる順は配列(sides)に定めた。トゥイーンを始める辺のインデックス(index)から、つぎの辺を順に時計回りに確かめる。つぎの辺のインデックス(indexNext)は、加算したうえで四辺(count)の剰余を得る。そうすることで、終わりのエレメントのつぎは初めに戻る。

ランダムな数値(position)がつぎの辺の長さ(length)より小さければ、その辺(side)がつぎの行き先に決まる。そこで、ふたつの数値がプロパティ(positionとside)に定められたオブジェクト(returnObject)を返す。ランダムな数値の方が大きければ、つぎの辺の長さを差引いて、ループ処理する。

function getNextPosition(mySide) {
  var index = sides.indexOf(mySide);
  var range = lengthsTotal - lengths[index];
  var position = range * Math.random();
  var returnObject;
  var count = lengths.length;
  for (var i = 1; i < count; i++) {
    var indexNext = (index + i) % count;
    var length = lengths[indexNext];
    if (position <= length) {
      var side = sides[indexNext];
      returnObject = {side:side, position:position};
      currentSide = side;
      break;
    } else {
      position -= length;
    }
  }
  return returnObject;
}

行き先の辺とピクセル位置が定められた引数(position)のオブジェクトからxy座標のPointインスタンスを返す関数(getPoint())は新たに定める。取出した辺(side)が水平("top"または"bottom")ならy座標、垂直("left"または"right")ならx座標が決まるので、ランダムなピクセル位置(value)はもう一方の座標になる。こうして定めたPointオブジェクト(myPoint)が関数から返される。

function getPoint(position) {
  var myPoint = new createjs.Point();
  var side = position.side;
  var value = position.position;
  switch (side) {
    case "top":
      myPoint.x = value + left;
      myPoint.y = top;
      break;
    case "bottom":
      myPoint.x = value + left;
      myPoint.y = bottom;
      break;
    case "left":
      myPoint.x = left;
      myPoint.y = value + top;
      break;
    case "right":
      myPoint.x = right;
      myPoint.y = value + top;
      break;
  }
  return myPoint;
}

これで、3辺のうえから偏りなくランダムな行き先が選ばれるので、その座標にトゥイーンを定めればよい。これらの手直しを加えたのが、以下のコード2だ。これで、オブジェクトはCanvasの四辺を、ランダムな時間とイージングでトゥイーンアニメーションし続けることになる図3⁠。

図3 四辺のランダムな位置・時間・イージングでトゥイーンする
図3 四辺のランダムな位置・時間・イージングでトゥイーンする
コード2 四辺をランダムな時間とイージングでトゥイーンアニメーションする
var stage;
var myBitmap;
var top = 0;
var bottom;
var left = 0;
var right;
var currentSide;
var easings = [createjs.Ease.circIn, createjs.Ease.bounceOut, createjs.Ease.elasticInOut];
var sides = ["left", "top", "right", "bottom"];
var lengths;
function initialize() {
  canvasObject = document.getElementById("myCanvas");
  var file = "images/Pen.png";
  var loader = new createjs.LoadQueue(false);
  right = canvasObject.width;
  bottom = canvasObject.height;
  stage = new createjs.Stage(canvasObject);
  loader.addEventListener("fileload", draw);
  loader.loadFile(file);
}
function draw(eventObject) {
  var myImage = eventObject.result;
  var halfWidth = myImage.width / 2;
  var halfHeight = myImage.height / 2;
  setGeometricData(halfWidth, halfHeight);
  myBitmap = new createjs.Bitmap(myImage);
  myBitmap.regX = halfWidth;
  myBitmap.regY = halfHeight;
  myBitmap.x = halfWidth;
  myBitmap.y = (top + bottom) / 2;
  stage.addChild(myBitmap);
  stage.update();
  setRandomTween(myBitmap, "left");
  createjs.Ticker.addEventListener("tick", stage);
}
function setGeometricData(offsetX, offsetY) {
  top += offsetY;
  bottom -= offsetY;
  left += offsetX;
  right -= offsetX;
  var width = right - left;
  var height = bottom - top;
  lengths = [height, width, height, width];
  lengthsTotal = height + width + height + width;
}
function setRandomTween(target, side) {
  var position = getNextPosition(side);
  var nextPoint = getPoint(position);
  var randomTime = Math.random() * 5000 + 1000;
  var easing = easings[Math.floor(Math.random() * easings.length)];
  setTween(target, nextPoint, randomTime, easing);
}
function setTween(target, myPoint, time, easing) {
  createjs.Tween.get(target)
  .to({x:myPoint.x, y:myPoint.y}, time, easing)
  .call(setRandomTween, [target, currentSide]);
}
function getNextPosition(mySide) {
  var index = sides.indexOf(mySide);
  var range = lengthsTotal - lengths[index];
  var position = range * Math.random();
  var returnObject;
  var count = lengths.length;
  for (var i = 1; i < count; i++) {
    var indexNext = (index + i) % count;
    var length = lengths[indexNext];
    if (position <= length) {
      var side = sides[indexNext];
      returnObject = {side:side, position:position};
      currentSide = side;
      break;
    } else {
      position -= length;
    }
  }
  return returnObject;
}
function getPoint(position) {
  var myPoint = new createjs.Point();
  var side = position.side;
  var value = position.position;
  switch (side) {
    case "top":
      myPoint.x = value + left;
      myPoint.y = top;
      break;
    case "bottom":
      myPoint.x = value + left;
      myPoint.y = bottom;
      break;
    case "left":
      myPoint.x = left;
      myPoint.y = value + top;
      break;
    case "right":
      myPoint.x = right;
      myPoint.y = value + top;
      break;
  }
  return myPoint;
}

これで予定したお題は仕上がった。CreateJSのバージョンが古い第1回とはもちろん、前回掲げたでき上がりのスクリプトとも細かいところが少し変わっているので、ご参考までにjsdo.itに改めてコードを加えておく。次回からは、また新たなお題に取組みたい。

おすすめ記事

記事・ニュース一覧