Away3D TypeScriptではじめる3次元表現

第17回ロゴがパーティクルで弾けるアニメーション

第11回から前回の第16回までで、パーティクルの作例Away3D 15/03/13: Animating particles simulating fire第16回サンプル1を仕上げた。今回から取り組むお題も、Away3D TypeScriptサイトの「Examples」からパーティクルの作例Exploding browser logos using particlesを参考にする図1⁠。

図1 ブラウザのロゴがパーティクルになって弾ける
図1 ブラウザのロゴがパーティクルになって弾ける

ロゴがひとつパーティクルで弾けるアニメーション

前のお題でわかったように、パーティクルをつくるにはアニメーションなどの下ごしらえが手間取り、順に進めていってもなかなか動きが目で確かめられるコードにならない。そこで、今回目指すパーティクルのアニメーションをひとつつくるまでのスクリプトは、初めにコード1としてまとめて示すことにする。

見てのとおり、すでにそれなりの行数だ。もっとも、前回のパーティクルのコードと仕組みが似ている部分も多い。それらも参考にしながら解説していこう。動きは、jsdo.itに掲げたサンプル1で確かめられる。なお、画像として読み込むChromeロゴのファイルは、awayjs-examplesの中のbin/assets/chrome.pngを用い、assetsフォルダに納めた図2⁠。

図2 Chromeロゴの画像ファイル
図2 Chromeロゴの画像ファイル
サンプル1 Away3D 15/03/13: Exploding a logo using particles
サンプル1 Away3D 15/03/13: Exploding a logo using particles
 クリックでjsdo.itサイトのサンプルが開く
コード1 ロゴひとつをパーティクルにしてアニメーションさせる
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var ColorTransform = require("awayjs-core/lib/geom/ColorTransform");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var View = require("awayjs-display/lib/containers/View");
var HoverController = require("awayjs-display/lib/controllers/HoverController");
var Mesh = require("awayjs-display/lib/entities/Mesh");
var PointLight = require("awayjs-display/lib/entities/PointLight");
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var Cast = require("awayjs-display/lib/utils/Cast");
var ParticleAnimationSet = require("awayjs-renderergl/lib/animators/ParticleAnimationSet");
var ParticleAnimator = require("awayjs-renderergl/lib/animators/ParticleAnimator");
var ParticlePropertiesMode = require("awayjs-renderergl/lib/animators/data/ParticlePropertiesMode");
var ParticleBillboardNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBillboardNode");
var ParticleBezierCurveNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBezierCurveNode");
var ParticleInitialColorNode = require("awayjs-renderergl/lib/animators/nodes/ParticleInitialColorNode");
var ParticlePositionNode = require("awayjs-renderergl/lib/animators/nodes/ParticlePositionNode");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var ParticleGeometryHelper = require("awayjs-renderergl/lib/utils/ParticleGeometryHelper");
var MethodMaterial = require("awayjs-methodmaterials/lib/MethodMaterial");
var MethodRendererPool = require("awayjs-methodmaterials/lib/pool/MethodRendererPool");
var PARTICLE_SIZE = 2;
var view;
var cameraController;
var light;
var assetsURL = "assets/chrome.png";
var bitmapData;
var colorValues = [];
var colorPoints = [];
var colorMaterial;
var colorAnimationSet;
var colorAnimator;
var timer;
var time = 0;
function initialize() {
  var lightPicker = createLights();
  view = createView(window.innerWidth, window.innerHeight, 0x0);
  cameraController = setupCameraController(view.camera, 1000, 225, 10);
  colorMaterial = createMaterial(lightPicker, true);
  setListeners();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer(MethodRendererPool);
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function setupCameraController(camera, distance, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function createLights() {
  light = createPointLight(0xFFFFFF, 1, 600, 100, 2);
  return new StaticLightPicker([light]);
}
function createPointLight(color, ambient, fallOff, radius, specular) {
  var light = new PointLight();
  light.color = color;
  light.ambient = ambient;
  light.fallOff = fallOff;
  light.radius = radius;
  light.specular = specular;
  return light;
}
function createMaterial(lightPicker, bothSides) {
  var material = new MethodMaterial();
  material.bothSides = bothSides;
  material.lightPicker = lightPicker;
  return material;
}
function onResourceComplete(eventObject) {
  var asset = eventObject.assets[0];
  bitmapData = Cast.bitmapData(asset);
  var colorGeometry = createParticles();
  startParticleAnimation(colorGeometry);
}
function createParticles() {
  colorAnimationSet = createColorAnimationSet(initColorParticle);
  setParticlesData(bitmapData, PARTICLE_SIZE, colorValues, colorPoints);
  var primitive = new PrimitivePlanePrefab(PARTICLE_SIZE, PARTICLE_SIZE, 1, 1, false);
  var geometry = primitive.geometry;
  var colorGeometrySet = [];
  var count = colorPoints.length;
  for (var j = 0; j < count; j++) {
    colorGeometrySet[j] = geometry;
  }
  var colorGeometry = ParticleGeometryHelper.generateGeometry(colorGeometrySet);
  return colorGeometry;
}
function setParticlesData(bitmapData, size, colorValues, colorPoints) {
  var bitmapWidth = bitmapData.width;
  var bitmapHeight = bitmapData.height;
  for (var i = 0; i < bitmapWidth; i++) {
    for (var j = 0; j < bitmapHeight; j++) {

      var point = new Vector3D(size * (i - bitmapWidth / 2), size * ( -j + bitmapHeight / 2));
      var color = bitmapData.getPixel32(i, j);
      var rgbColor = getRgbComponents(color);
      rgbColor.scaleBy(1 / 255);
      colorValues.push(rgbColor);
      colorPoints.push(point);
    }
  }
}
function getRgbComponents(rgbColor) {
  var rgbVector = new Vector3D();
  rgbVector.x = (rgbColor & 0xff0000) >> 16;
  rgbVector.y = (rgbColor & 0xff00) >> 8;
  rgbVector.z = rgbColor & 0xff;
  return rgbVector;
}
function createColorAnimationSet(initParticleFunc) {
  var LOCAL_STATIC = ParticlePropertiesMode.LOCAL_STATIC;
  var colorAnimationSet = new ParticleAnimationSet();
  colorAnimationSet.addAnimation(new ParticleBillboardNode());
  colorAnimationSet.addAnimation(new ParticleBezierCurveNode(LOCAL_STATIC));
  colorAnimationSet.addAnimation(new ParticlePositionNode(LOCAL_STATIC));
  colorAnimationSet.addAnimation(new ParticleInitialColorNode(LOCAL_STATIC, true, false, new ColorTransform(0, 1, 0, 1)));
  colorAnimationSet.initParticleFunc = initParticleFunc;
  return colorAnimationSet;
}
function startParticleAnimation(colorGeometry) {
  var scene = view.scene;
  var colorParticleMesh = new Mesh(colorGeometry, colorMaterial);
  var animator = new ParticleAnimator(colorAnimationSet);
  colorAnimator = animator;
  colorParticleMesh.animator = animator;
  scene.addChild(colorParticleMesh);
}
function setListeners() {
  timer = new RequestAnimationFrame(render);
  timer.start();
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(assetsURL));
}
function initColorParticle(properties) {
  var BEZIER_END_VECTOR3D = ParticleBezierCurveNode.BEZIER_END_VECTOR3D;

  var index = properties.index;
  var endPoint = new Vector3D();
  var rgb = colorValues[index];
  properties.startTime = 0;
  properties.duration = 1;
  properties[BEZIER_END_VECTOR3D] = endPoint;
  properties[ParticleInitialColorNode.COLOR_INITIAL_COLORTRANSFORM] = new ColorTransform(rgb.x, rgb.y, rgb.z, 1);
  properties[ParticleBezierCurveNode.BEZIER_CONTROL_VECTOR3D] = getRandomVector3D(100);
  properties[ParticlePositionNode.POSITION_VECTOR3D] = colorPoints[index];
}
function getRandomVector3D(radius) {
  var angle0 = Math.random() * Math.PI * 2;
  var angle1 = Math.random() * Math.PI * 2;
  var x = radius * Math.cos(angle0) * Math.cos(angle1);
  var y = radius * Math.cos(angle0) * Math.sin(angle1);
  var z = radius * Math.sin(angle0);
  return new Vector3D(x, y, z);
}
function render(deltaTime) {
  time += deltaTime;
  if (colorAnimator) {
    var _time = 1000 * (Math.sin(time / 5000) + 1);
    colorAnimator.update(_time);
  }
  view.render();
}

初期設定で呼び出す関数

前掲コード1の初期設定の関数(initialize())は、つぎの5つの関数を呼び出している。これらの関数の中のcreateView()は、第16回コード3とまったく同じである。また、関数setupCameraController()も引数とプロパティの設定が減っただけだ。この2つの関数は抜き書きしないので、前掲コード1を直接参照してほしい。

  • createLights()
  • createView()
  • setupCameraController()
  • createMaterial()
  • setListeners()

関数createLights()は、つぎのように光源をつくったうえでStaticLightPickerオブジェクトに納めて返す。ただし、光源はパーティクルを照らすだけなので、平行光源(DirectionalLightオブジェクト)は使わず、PointLightオブジェクトをつくっている(第14回炎の光を加える参照⁠⁠。PointLightオブジェクトは、後に複数つくることになるため、関数(createPointLight())を分けた。PointLightオブジェクトに定めているプロパティは、すでにこの連載で紹介したので、以下に表1としてまとめておこう。

var light;

function initialize() {
  var lightPicker = createLights();

}

function createLights() {
  light = createPointLight(0xFFFFFF, 1, 600, 100, 2);
  return new StaticLightPicker([light]);
}
function createPointLight(color, ambient, fallOff, radius, specular) {
  var light = new PointLight();
  light.color = color;
  light.ambient = ambient;
  light.fallOff = fallOff;
  light.radius = radius;
  light.specular = specular;
  return light;
}
表1 LightBaseクラスのプロパティ
LightBaseクラスのプロパティ値と機能
ambient環境光の強さを示す0以上1以下の数値(デフォルト値0)
ambientColor環境光のカラー値を示す0から0xFFFFFFまでの整数(デフォルト値0xFFFFFF)
color光のカラー値(デフォルト値0xFFFFFF)
diffuse光の拡散する強さを示す0以上の数値(デフォルト値1)
fallOff光が届く距離の最大値(デフォルト値10000)
radius光が届く距離の最小値(デフォルト値9000)
specular光の反射する強さを示す0以上の数値(デフォルト値1)

関数createMaterial()は、つぎのようにマテリアル(MethodMaterialオブジェクト)をつくって返す。MaterialBase.bothSidesプロパティは、マテリアルのカメラと反対側の面を描画するかどうかを定める。負荷を減らすため、描画しないfalseがデフォルト値だ。このプロパティをtrueとし、MaterialBase.lightPickerプロパティには、PointLightオブジェクトが納められたStaticLightPickerを与えた。

var colorMaterial;

function initialize() {

  colorMaterial = createMaterial(lightPicker, true);

}

function createMaterial(lightPicker, bothSides) {
  var material = new MethodMaterial();
  material.bothSides = bothSides;
  material.lightPicker = lightPicker;
  return material;
}

関数setListeners()の仕事のひとつは、ロゴの画像ファイル(assetsURL)AssetLibrary.load()メソッドでロードし、読み込み終えたときLoaderEvent.RESOURCE_COMPLETEイベント)のリスナー関数(onResourceComplete())を定めることだ。この関数は、ロードした素材をCast.bitmapData()メソッドの引数に渡して、BitmapDataオブジェクトを得る。このオブジェクトからは、ピクセル単位でカラー値が調べられる。そのカラーをパーティクルひとつひとつに当てようという目論見だ。

var assetsURL = "assets/chrome.png";
var bitmapData;

function initialize() {

  setListeners();
}

function setListeners() {

  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(assetsURL));
}

function onResourceComplete(eventObject) {
  var asset = eventObject.assets[0];
  bitmapData = Cast.bitmapData(asset);
  var colorGeometry = createParticles();
  startParticleAnimation(colorGeometry);
}

素材を読み込み終えたときのリスナー関数(onResourceComplete())からは、さらにつぎの2つの関数が呼び出される。ひとつは、パーティクルの幾何学情報のGeometryオブジェクトやそれらのカラーと位置、およびアニメーションのデータをつくる関数(createParticles())だ。そしてもうひとつは、Meshオブジェクトをつくって、パーティクルのアニメーションを初期設定する関数(startParticleAnimation())になる。

  • createParticles()
  • startParticleAnimation()

パーティクルとアニメーションを定める関数

関数createParticles()は、つぎの2つの関数を呼び出したあと、以下のようにパーティクルのParticleGeometryオブジェクト(colorGeometry)をつくって返す。呼び出したひとつ目の関数(createColorAnimationSet())は、ParticleAnimationSetオブジェクトを返すので、それが変数(colorAnimationSet)に与えられる。2つ目の関数(setParticlesData())は、読み込んだロゴのBitmapDataオブジェクトからピクセルごとのカラー値と位置座標をそれぞれ変数の配列(colorValuesとcolorPoints)に納める。

  • createColorAnimationSet()
  • setParticlesData()

パーティクルの幾何学情報のGeometryオブジェクト(geometry)PrimitivePrefabBase.geometryプロパティから得た。Geometryオブジェクトの配列(colorGeometrySet)は、ParticleGeometryHelper.generateGeometry()メソッドに渡すと、ParticleGeometryオブジェクトができる(第12回パーティクルのオブジェクトを加える参照⁠⁠。これとMethodMaterialオブジェクト(前述変数colorMaterial)Mesh()コンストラクタに渡せば、パーティクルの平面オブジェクトがつくれる。なお、配列に納めるGeometryオブジェクトの数はロゴのBitmapDataオブジェクトの総ピクセル数(count)だ。

var PARTICLE_SIZE = 2;

var colorValues = [];
var colorPoints = [];

var colorAnimationSet;

function createParticles() {
  colorAnimationSet = createColorAnimationSet(initColorParticle);
  setParticlesData(bitmapData, PARTICLE_SIZE, colorValues, colorPoints);
  var primitive = new PrimitivePlanePrefab(PARTICLE_SIZE, PARTICLE_SIZE, 1, 1, false);
  var geometry = primitive.geometry;
  var colorGeometrySet = [];
  var count = colorPoints.length;
  for (var j = 0; j < count; j++) {
    colorGeometrySet[j] = geometry;
  }
  var colorGeometry = ParticleGeometryHelper.generateGeometry(colorGeometrySet);
  return colorGeometry;
}

関数createColorAnimationSet()は、以下のとおりParticleAnimationSetオブジェクト(colorAnimationSet)をつくって返す。オブジェクトには、AnimationNodeBase(のサブクラス)のオブジェクトをParticleAnimationSet.addAnimation()メソッドで加える(第12回パーティクルにアニメーションを定めるおよび第13回パーティクルのカラーをアニメーションさせる参照⁠⁠。ParticleNodeBaseのサブクラスのコンストラクタを、前回のパーティクルのお題ですでに用いたものも含めて表2にまとめた(なお、ParticleNodeBaseはAnimationNodeBaseのサブクラスだ⁠⁠。関数が引数に受け取った初期化の関数(initParticleFunc)は、ParticleAnimationSet.initParticleFuncプロパティに定めた。

function createColorAnimationSet(initParticleFunc) {
  var LOCAL_STATIC = ParticlePropertiesMode.LOCAL_STATIC;
  var colorAnimationSet = new ParticleAnimationSet();
  colorAnimationSet.addAnimation(new ParticleBillboardNode());
  colorAnimationSet.addAnimation(new ParticleBezierCurveNode(LOCAL_STATIC));
  colorAnimationSet.addAnimation(new ParticlePositionNode(LOCAL_STATIC));
  colorAnimationSet.addAnimation(new ParticleInitialColorNode(LOCAL_STATIC, true, false, new ColorTransform(0, 1, 0, 1)));
  colorAnimationSet.initParticleFunc = initParticleFunc;
  return colorAnimationSet;
}
表2 ParticleNodeBaseのサブクラスのコンストラクタ
ParticleNodeBaseのサブクラスのコンストラクタ機能
ParticleBezierCurveNode(モード, コントロールポイント, 終点)時間に応じた位置をベジエ曲線で定める
ParticleBillboardNode()パーティクルの角度をつねにカメラに向ける。
ParticleColorNode(モード, 乗数データの使用, オフセットデータの使用, usesCycleの使用, usesPhaseの使用, 初めの色, 終わりの色)パーティクルアニメーションの時間に応じた色の変わり方を定める
ParticleInitialColorNode(モード, 乗数データの使用, オフセットデータの使用, 初めの色)パーティクルの初めの色を定める
ParticlePositionNode(モード, 位置ベクトル)パーティクルの初めの位置を定める
ParticleScaleNode(モード, usesCycleの使用, usesPhaseの使用, 最小伸縮率, 最大伸縮率)パーティクルアニメーションが時間に応じてどう伸縮するかを定める
ParticleVelocityNode(モード, 速度ベクトル)パーティクルアニメーションが始まるときの速度を定める

関数setParticlesData()には、つぎのように4つの引数を渡す。第1引数のBitmapDataオブジェクトと第2引数のパーティクルの大きさにもとづいて、第3引数と第4引数の配列にそれぞれピクセルごとのカラー値と位置座標を納める。2つの配列は空のまま受け取る前提だ。

setParticlesData(BitmapDataオブジェクト, パーティクルの大きさ, カラー値の配列, 位置の配列)

BitmapDataオブジェクトのピクセルごとのカラー値(color)は、水平と垂直のふたつのインデックスを引数にしてBitmapData.getPixel32()メソッドで調べられる。座標(point)は、BitmapDataオブジェクト(bitmapData)の中心を原点(0, 0)として関数の第2引数の大きさ(size)に比例させ、Vector3Dオブジェクト(point)で定めた(z座標はデフォルト値0⁠⁠。

関数getRgbComponents()は、カラー値の整数からRGB成分値を取り出し(⁠⁠2進数・16進数とビット演算」05カラー値とビット演算参照⁠⁠、Vector3Dオブジェクトのxyzプロパティに納めて返す。なお、関数setParticlesData()では、受け取ったオブジェクトにVector3D.scaleBy()メソッドを用いて、256階調の値を0~1の比率に変えた。これは、後でパーティクルを初期化するとき(initColorParticle()関数⁠⁠、カラーをColorTransformオブジェクトで定めるためだ(第13回パーティクルのカラーをアニメーションさせる参照⁠⁠。こうして得られたピクセルごとのカラーと座標は、それぞれ配列の変数(colorValuesとcolorPoints)に納めている。

function setParticlesData(bitmapData, size, colorValues, colorPoints) {
  var bitmapWidth = bitmapData.width;
  var bitmapHeight = bitmapData.height;
  for (var i = 0; i < bitmapWidth; i++) {
    for (var j = 0; j < bitmapHeight; j++) {
      var point = new Vector3D(size * (i - bitmapWidth / 2), size * ( -j + bitmapHeight / 2));
      var color = bitmapData.getPixel32(i, j);
      var rgbColor = getRgbComponents(color);
      rgbColor.scaleBy(1 / 255);
      colorValues.push(rgbColor);
      colorPoints.push(point);
    }
  }
}
function getRgbComponents(rgbColor) {
  var rgbVector = new Vector3D();
  rgbVector.x = (rgbColor & 0xff0000) >> 16;
  rgbVector.y = (rgbColor & 0xff00) >> 8;
  rgbVector.z = rgbColor & 0xff;
  return rgbVector;
}

関数startParticleAnimation()は、パーティクルのアニメーションを初期設定する。引数に受け取ったParticleGeometryオブジェクト(colorGeometry)とMethodMaterialオブジェクト(colorMaterial)をMesh()コンストラクタに渡して、パーティクルのオブジェクト(colorParticleMesh)がつくられる。そのMesh.animatorプロパティにParticleAnimatorオブジェクト(animator)を定めたうえで、SceneオブジェクトにScene.addChild()メソッドで加えた。

function startParticleAnimation(colorGeometry) {
  var scene = view.scene;
  var colorParticleMesh = new Mesh(colorGeometry, colorMaterial);
  var animator = new ParticleAnimator(colorAnimationSet);
  colorAnimator = animator;
  colorParticleMesh.animator = animator;
  scene.addChild(colorParticleMesh);
}

パーティクルを初期化する関数

パーティクルを初期化する関数は、前述のParticleAnimationSetオブジェクトをつくる関数(createColorAnimationSet())ParticleAnimationSet.initParticleFuncプロパティに定めたinitColorParticle()だ。

プロパティParticleProperties.startTimeParticleProperties.durationについてはよいだろう(第12回パーティクルのアニメーションを動かす参照⁠⁠。パーティクルのカラーと座標は、配列の変数(colorValuesとcolorPoints)からインデックス(index)にしたがって取り出し、それぞれ引数のオブジェクト(properties)ParticleInitialColorNode.COLOR_INITIAL_COLORTRANSFORMParticlePositionNode.POSITION_VECTOR3Dプロパティに定める。

今回のアニメーションの動きを決める鍵となるのは、ParticleBezierCurveNodeのプロパティの定めだ。名前から想像がつくように、ベジエ曲線で動き方を決める。その終点(アンカーポイント)ParticleBezierCurveNode.BEZIER_END_VECTOR3DコントロールポイントがParticleBezierCurveNode.BEZIER_CONTROL_VECTOR3Dで、ともにVector3Dオブジェクトを与える。前者(endPoint)は引数なしのVector3D()コンストラクタでつくられた値なのでデフォルト値の原点(0, 0, 0⁠⁠、つまりもとの位置だ。後者は、別に定めた関数getRandomVector3D()により、引数の距離以内のランダムなVector3Dオブジェクトが与えられる。

すると、パーティクルのアニメーションは、コントロールポイントにより一旦初めの位置から遠ざかり、その後終点に定めたもとの位置に戻ってくることになる。なお、関数getRandomVector3D()の距離とふたつの角度から座標を求める計算については、第13回パーティクルの動きにランダムな広がりを与えるを参照してほしい。

function initColorParticle(properties) {
  var BEZIER_END_VECTOR3D = ParticleBezierCurveNode.BEZIER_END_VECTOR3D;

  var index = properties.index;
  var endPoint = new Vector3D();
  var rgb = colorValues[index];
  properties.startTime = 0;
  properties.duration = 1;
  properties[BEZIER_END_VECTOR3D] = endPoint;
  properties[ParticleInitialColorNode.COLOR_INITIAL_COLORTRANSFORM] = new ColorTransform(rgb.x, rgb.y, rgb.z, 1);
  properties[ParticleBezierCurveNode.BEZIER_CONTROL_VECTOR3D] = getRandomVector3D(100);
  properties[ParticlePositionNode.POSITION_VECTOR3D] = colorPoints[index];
}
function getRandomVector3D(radius) {
  var angle0 = Math.random() * Math.PI * 2;
  var angle1 = Math.random() * Math.PI * 2;
  var x = radius * Math.cos(angle0) * Math.cos(angle1);
  var y = radius * Math.cos(angle0) * Math.sin(angle1);
  var z = radius * Math.sin(angle0);
  return new Vector3D(x, y, z);
}

パーティクルをアニメーションさせる関数

関数setListeners()は、RequestAnimationFrameオブジェクトでアニメーションのコールバック関数render()を定めている。この関数からAnimatorBase.update()メソッドを呼び出した。総経過時間を引数として、アニメーションの状態が改められる。これがこのアニメーションの2つ目の鍵だ。

RequestAnimationFrame()コンストラクタに渡したコールバック関数(render())は、引数として前回の呼び出しからの経過時間(deltaTime)を受け取る。その値を変数(time)に加えて、総経過時間をもたせた。ところが、AnimatorBase.update()メソッドにはこの値を直に与えず、Math.sin()メソッドで手を加えた値(_time)が渡されている。この式が、初めに掲げたサンプル1の動きをつくっているのだ。

var time = 0;

function setListeners() {
  timer = new RequestAnimationFrame(render);
  timer.start();

}

function render(deltaTime) {
  time += deltaTime;
  if (colorAnimator) {
    var _time = 1000 * (Math.sin(time / 5000) + 1);
    colorAnimator.update(_time);
  }
  view.render();
}

試しに、前掲コードをつぎのように書き替えて、AnimatorBase.update()メソッドに総経過時間(time)に調整係数を乗じて引数として渡してみよう。前述のとおり、ParticleBezierCurveNodeのプロパティでベジエ曲線のコントロールポイントをランダムな距離で外側に定めたため、パーティクルはばらばらに広がり出す。だが、終点(アンカーポイント)はもとの位置なので、やがて引き戻されていく。そして、その先は慣性にしたがってそのまま進み続け、やがて画面の外に消えてしまう。これが、総経過時間をひたすら増やした結果だ。

function render(deltaTime) {
  time += deltaTime;

  // var _time = 1000 * (Math.sin(time / 5000) + 1);
  // colorAnimator.update(_time);
  colorAnimator.update(time / 5);

}

三角関数のsinは、物理のバネ運動の式に用いられ、±1の範囲で増減を繰り返す図3⁠。前掲の式では1を加えているので、0~2の間をバネのように変化する。さらに調整係数で、変化の速さと幅を決めた結果、0から一定時間の間を行ったり来たりすることになった。そのため、パーティクルが広がったり、縮まったりというアニメーションを繰り返したのだ。

図3 sin関数はバネ運動のように値の増減を繰り返す
図3 sin関数はバネ運動のように値の増減を繰り返す
WikipediaHarmonic oscillatorより。

ようやく、ロゴひとつをパーティクルでアニメーションさせるコード1が説明し終えた。jsdo.itに掲げたサンプル1も試してみるとよいだろう。

次回からは、書き加えたコードの動きを確かめながら解説する。実は、今回のパーティクルのお題が、この連載の結びとなる。あとしばらくおつき合い願いたい。

おすすめ記事

記事・ニュース一覧