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

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

この記事を読むのに必要な時間:およそ 5 分

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

前掲のコードを試してみると,描かれる線の動きが少しぎこちない。これは,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;
}
※1
requestAnimationFrameのAPIをサポートしていないブラウザもある。その場合には,デフォルト値のTicker.TIMEOUTモードが用いられる。

※2
jsdo.itのコードでは,window.innerWidthwindow.innerHeightプロパティの値をCanvas領域に定めて,ウィンドウ内一杯に線が描けるようにした(第17~18行目⁠⁠。
var canvasElement = document.getElementById("myCanvas");
canvasElement.width = window.innerWidth;
canvasElement.height = window.innerHeight;

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

最初の項「弾みのついた軌跡を描く」で,線描の関数(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の式は,このように加速度を用いて,バネのような動きをつくりだしたのだ。

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

著者プロフィール

野中文雄(のなかふみお)

ソフトウェアトレーナー,テクニカルライター,オーサリングエンジニア。上智大学法学部卒,慶応義塾大学大学院経営管理研究科修士課程修了(MBA)。独立系パソコン販売会社で,総務・人事,企画,外資系企業担当営業などに携わる。その後,マルチメディアコンテンツ制作会社に転職。ソフトウェアトレーニング,コンテンツ制作などの業務を担当する。2001年11月に独立。Web制作者に向けた情報発信プロジェクトF-siteにも参加する。株式会社ロクナナ取締役(非常勤)。

URLhttp://www.FumioNonaka.com/

著書