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

第24回マウスポインタの動きに弾みがついた曲線を滑らかに描く

前回の第23回マウスポインタの軌跡を滑らかな線で描きながら消すは、マウスポインタの動きに沿って、滑らかに描かれては消える線のアニメーションをつくった。その線は、マウスポインタの動きに少し遅れて描かれた。今回は、線の表現にもう少し手を入れて仕上げよう。

弾みのついた軌跡を描く

マウスポインタの動きに少し遅れて、減速しながら目的の位置にたどりつく、いわゆるイーズアウトの動きはインタラクティブなアニメーションでよく見かける。けれど、今回はむしろ線の描き方に弾みをつけたい。勢い余って目的地を通り越す、バネやゴムの動きだ。

第23回コード2マウスポインタの動きに沿って描いた線を後から順に消してゆくに手を加えてゆこう。Ticker.tickイベントのリスナー関数(draw())をつぎのように書き改めた。新たな変数(friction)もひとつ定めた。これで、マウスポインタの動きに勢いの加わった線が描かれる。

var lastMidPoint = new createjs.Point();
var currentPoint = new createjs.Point();
var lastPoint = new createjs.Point();
var velocityX = 0;
var velocityY = 0;
var ease = 0.25;
var friction = 0.75;

function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);

  if (moveX * moveX + moveY * moveY > 0.1) {
    // velocityX = moveX * ease;
    // velocityY = moveY * ease;
    velocityX += moveX * ease;
    velocityY += moveY * ease;
    velocityX *= friction;
    velocityY *= friction;
    currentPoint.x += velocityX;
    currentPoint.y += velocityY;
    var midPoint = new createjs.Point((lastPoint.x + currentPoint.x) / 2, (lastPoint.y + currentPoint.y) / 2);
    var myShape = getNewChild();
    container.addChild(myShape);
    drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);
    lastPoint.initialize(currentPoint.x, currentPoint.y);
    lastMidPoint.initialize(midPoint.x, midPoint.y);

  }
  stage.update();
}

第23回コード2で、ある位置からつぎの位置に動くとき、縦軸は位置、横軸を時間としたグラフを描くとつぎの図1のようになる。時間が経つにしたがって、位置は目標に近づく。ただし、目標に近づくにつれ、その動きは遅くなっている。そのため、第23回コード2では、マウスポインタに少し遅れながら線が描かれた。

図1 動きが遅くなりながら目標に近づく
図1 動きが遅くなりながら目標に近づく

それに対して、前掲のコードでは、初めは目標の位置を通り越す。しかし、バネのように戻っては通り過ぎる動きを繰り返して、次第に目標値に近づいてゆく図2⁠。こうすると、マウスポインタの動きに弾みのついた線が描かれることになる。第23回コード2の式との数学的な意味の違いは、後に項を改めて解説する。

図2 目標を通り過ぎては戻るバネのような動き
図2 目標を通り過ぎては戻るバネのような動き

アニメーションを再描画するタイミング

前掲のコードを試してみると、描かれる線の動きが少しぎこちない。これは、Ticker.tickイベントのフレームレートがデフォルト値の20fps(50ミリ秒間隔)のままで低いためだ。そうであれば、フレームレートを上げればよい。フレームレートはTicker.setFPS()メソッドで変えられ、ミリ秒間隔で決めるならTicker.setInterval()メソッドが使える。だが今回は、イベントのタイミングをEaselJS 0.7.0から備わったrequestAnimationFrameのAPIで定めることにする。

Ticker.tickイベントはデフォルトでは、内部的にwindow.setTimeout()メソッドで配信される。このメソッドはミリ秒の経過にもとづいて、コールバック関数を呼出す。その時間間隔にはばらつきが少ない。window.requestAnimationFrame()メソッドは、間隔にはばらつきがあるものの、ブラウザが描画できるようになったときにコールバックを呼び出す。したがって、アニメーションを描画するのに適している。一般的な画面のリフレッシュレート60Hzにもとづくと、フレームレートは約60fpsだ。

Ticker.tickイベントのタイミングは、Ticker.timingModeプロパティで切り替える。プロパティに定数Ticker.RAF(デフォルト値はTicker.TIMEOUTを与えれば、requestAnimationFrameのAPIが用いられる[1]⁠。つぎのように、Ticker.timingModeプロパティを定めよう。結果としてフレームレートが上がるので、描く線の数(maxLines)も増やすことにした。

var maxLines = 100;   // 50;

function initialize() {

  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", draw); 
}

第23回コード2に手を加えてでき上がったのが、以下のコード1だ。描かれる曲線には、マウスポインタの動きに対してバネのような弾みがつく図3⁠。また、描線の反応も速い。今回のお題は、これで仕上がりとしたい。jsdo.itにもコードを掲げた[2]⁠。

図3 描かれる線にはバネのような弾みがつく
図3 描かれる線にはバネのような弾みがつく
コード1 マウスポインタの動きに弾みがついた曲線を滑らかに描く
var stage;
var container;
var children = [];
var lastMidPoint = new createjs.Point();
var currentPoint = new createjs.Point();
var lastPoint = new createjs.Point();
var velocityX = 0;
var velocityY = 0;
var ease = 0.25;
var friction = 0.75;
var maxLines = 100;
var currentLineThickness = 1;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  container = new createjs.Container(); 
  stage.addChild(container);
  lastPoint.x = lastMidPoint.x = canvasElement.width / 2;
  lastPoint.y = lastMidPoint.y = canvasElement.height / 2;
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", draw); 
}
function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);
  var numChildren = container.getNumChildren();
  if (moveX * moveX + moveY * moveY > 0.1) {
    velocityX += moveX * ease;
    velocityY += moveY * ease;
    velocityX *= friction;
    velocityY *= friction;
    currentPoint.x += velocityX;
    currentPoint.y += velocityY;
    var midPoint = new createjs.Point((lastPoint.x + currentPoint.x) / 2, (lastPoint.y + currentPoint.y) / 2);
    var myShape = getNewChild();
    container.addChild(myShape);
    drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);
    lastPoint.initialize(currentPoint.x, currentPoint.y);
    lastMidPoint.initialize(midPoint.x, midPoint.y);
    if (numChildren >= maxLines){
      removeOldChild();
    }
  } else if (numChildren > 1) {
    removeOldChild();
  }
  stage.update();
}
function getNewChild() {
  var child;
  if (children.length) {
    child = children.pop();
    child.graphics.clear();
  } else {
    child = new createjs.Shape();
  }
  return child;
}
function removeOldChild() {
  var child = container.getChildAt(0);
  container.removeChildAt(0);
  children.push(child);
}
function drawCurve(myGraphics, oldPoint, newPoint, controlPoint) {
  setLineThickness(oldPoint, newPoint);
  myGraphics.beginStroke("black")
  .setStrokeStyle(currentLineThickness, "round", "round")
  .moveTo(oldPoint.x, oldPoint.y)
  .quadraticCurveTo(controlPoint.x, controlPoint.y, newPoint.x, newPoint.y); 
}
function setLineThickness(oldPoint, newPoint) {
  var distanceX = newPoint.x - oldPoint.x;
  var distanceY = newPoint.y - oldPoint.y;
  var distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  var lineThickness = distance * 0.2;
  currentLineThickness += (lineThickness - currentLineThickness) * 0.25;
}

バネのような動きを数学の目で確かめる

最初の項「弾みのついた軌跡を描く」で、線描の関数(draw())がバネのような動きを表すと説明した。第23回コード2の速度を落としながら近づく式とどう違うのか、数学の目で確かめてみよう。

第23回「マウスポインタの軌跡を滑らかな線で描きながら消す」値の変化のさせ方を考えるで述べたとおり、新旧ポインタ座標の差(moveXとmoveY)を速度(velocityXとvelocityY)として、そのまま現在の位置座標に加えれば座標は今のポインタ位置に動く。その速度に減速率(ease)を乗じることで、ポインタに遅れて後を追い、近づくにつれ速度が落ちる(前掲図1参照⁠⁠。

var ease = 0.25;

function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);

  velocityX = moveX * ease;
  velocityY = moveY * ease;
  currentPoint.x += velocityX;
  currentPoint.y += velocityY;

}

それに対して、前掲コード1では、新旧マウスポインタ座標の差(moveXとmoveY)に減速率(ease)を乗じて、そのまま速度にするのではなく、速度(velocityXとvelocityY)に加えた。速度に加える数値は、文字どおり加速度になる。重力加速度という語があるように、加速度は力を表す。ふたつの座標の距離が離れるほど、近づこうとする力を強めるのはバネの性質だ。

もっとも、その加速度が与えられた速度をそのまま位置座標に加え続けると、いつまでもバネの伸縮は止まらない。そこで、速度に減衰(摩擦)係数(friction)を乗じた。すると、伸び縮みの幅が少しずつ減って、ついには目標の値に落着く(前掲図2参照⁠⁠。

var ease = 0.25;
var friction = 0.75;

function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);

  // velocityX = moveX * ease;
  // velocityY = moveY * ease;
  velocityX += moveX * ease;
  velocityY += moveY * ease;
  velocityX *= friction;
  velocityY *= friction;
  currentPoint.x += velocityX;
  currentPoint.y += velocityY;

}

第23回コード2は位置に速度を加えてアニメーションさせた。前掲コード1もそれは同じだ。けれど、コード1は速度に与える加速度を位置に応じて変えている。速度と加速度はよく区別してほしい。

コード1の加速度は、目標値(均衡点)から離れるほど、目標値に向けて力の強さを増す。目標値から遠ざかる速度は、加速度に引き戻されるため、次第に遅くなる。そして、速度がついに0になったとき、目標値からもっとも離れた位置の加速度は最大になる。その後、加速度に引戻されて速度は向きを変え、目標値に近づいてゆく。加速度を追い風にして速度は次第に増す。加速度は逆に、目標値に近づくにつれて小さくなる。そして、目標値に達したとき、速度は最高に、加速度は0になるのだ。

この繰返しにより、目標値を通り越しては戻るというバネの動きになる。そして、速度を摩擦係数で少しずつ下げると、揺れ幅が小さくなりながら、やがて目標値にたどり着く。コード1の式は、このように加速度を用いて、バネのような動きをつくりだしたのだ。

加速度を速度に加え、その速度を位置に加えて動きを表すのは、⁠オイラー法」という数学の考え方にもとづく。バネの動きとオイラー法について、さらに詳しくはバネのような動きを加速度から定める ー オイラー法をお読みいただきたい。

おすすめ記事

記事・ニュース一覧