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

第9回マウスイベントを扱う

前回の第8回オブジェクトの位置と大きさをランダムにするでは、真ん中のオブジェクトの周りに、ランダムな位置と大きさで複数のオブジェクトを置いて、カメラで回り込んだ。今回は、これらのオブジェクトへのマウスインタラクションを加えよう。オブジェクトのマウスイベントをどう扱うかが課題だ。

オブジェクトへのマウスクリックでURLを開く

まず、立方体をクリックしたとき、そのオブジェクトに割り当てたURLを開きたい。それぞれが開くURLは、あらかじめオブジェクトに定める。扱いやすいように、URLの文字列は配列にまとめて変数(urls)に納めておくとよい。初期設定の関数(initialize())が真ん中のオブジェクトにURLをプロパティ(url)として加えたうえ、オブジェクトを複製する関数(cloneMesh())でも同じ名前のプロパティに配列エレメントのURLを与えた。

var urls = [
  "http://gihyo.jp/design/serial/01/away3d-typescript",
  "http://gihyo.jp/design/serial/01/createjs",
  "http://typescript.away3d.com",
  "https://developer.mozilla.org/ja/docs/Web/JavaScript",
  "http://createjs.com/#!/Home"
];
function initialize() {

  cube.url = "http://fumiononaka.com";

  // cloneMesh(cube, 5);
  cloneMesh(cube, urls);

}

// function cloneMesh(mesh, count) {
function cloneMesh(mesh, urls) {

  var count = urls.length;
  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();

    clone.url = urls[i];

}

さて、マウスによるクリックはイベント定数MouseEvent.CLICKで扱う。お約束どおり、EventDispatcher.addEventListener()メソッドでリスナー関数を加え、その中にオブジェクトがクリックされたときの処理を書けばよい。

オブジェクト.addEventListener(MouseEvent.CLICK, リスナー関数)

オブジェクトをつくる関数(createCube())と複製する関数(cloneMesh())が定めたリスナー(onClick())は、クリックしたオブジェクトに割り当てられたURLを開く。マウスイベントの起こったオブジェクトは、リスナー関数が引数に受取ったイベントオブジェクト(eventObject)objectプロパティから得られる[1]⁠。そこで、オブジェクトのプロパティ(url)に定めておいたURLを、window.open()メソッドで開けばよい。

var MouseEvent = require("awayjs-display/lib/events/MouseEvent");

function createCube(width, height, depth, light) {

  cube.addEventListener(MouseEvent.CLICK, onClick); 

}

function cloneMesh(mesh, urls) {

  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();

    clone.addEventListener(MouseEvent.CLICK, onClick); 

  }
}
function onClick(eventObject) {
  var mesh = eventObject.object;
  window.open(mesh.url);
}

これで、オブジェクトをクリックすると、それぞれに定められたURLのサイトが開く図1⁠。まとめたスクリプトは、以下のコード1のとおりだ。

図1 立方体のオブジェクトをクリックするとそれぞれ異なるサイトが開く
図1 立方体のオブジェクトをクリックするとそれぞれ異なるサイトが開く
コード1 オブジェクトをクリックするとそれぞれに定められたURLが開く
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var Matrix3D = require("awayjs-core/lib/geom/Matrix3D");
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 DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var MouseEvent = require("awayjs-display/lib/events/MouseEvent");
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitiveCubePrefab = require("awayjs-display/lib/prefabs/PrimitiveCubePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var cube;
var imageDiffuse = "assets/trinket_diffuse.jpg";
var timer;
var ORIGIN = new Vector3D();
var angle = -Math.PI / 2;
var distance = 1500;
var stageWidth = 240;
var stageHeight = 180;
var urls = [
  "http://gihyo.jp/design/serial/01/away3d-typescript",
  "http://gihyo.jp/design/serial/01/createjs",
  "http://typescript.away3d.com",
  "https://developer.mozilla.org/ja/docs/Web/JavaScript",
  "http://createjs.com/#!/Home"
];
function initialize() {
  var directionalLight = createDirectionalLight(0.5, 0xFFFFFF);
  view = createView(stageWidth, stageHeight, 0x0);
  cube = createCube(400, 400, 400, directionalLight);
  setCamera(view.camera, distance, angle);
  cube.url = "http://fumiononaka.com";
  view.scene.addChild(cube);
  cloneMesh(cube, urls);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  timer = new RequestAnimationFrame(rotate);
  timer.start();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer();
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createCube(width, height, depth, light) {
  var material = new TriangleMethodMaterial();
  var cube = new PrimitiveCubePrefab(width, height, depth, 1, 1, 1, false)
  .getNewObject();
  cube.material = material;
  cube.addEventListener(MouseEvent.CLICK, onClick);
  material.lightPicker = new StaticLightPicker([light]);
  return cube;
}
function cloneMesh(mesh, urls) {
  var scene = view.scene;
  var count = urls.length;
  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();
    var distance = getRandom(500, 1200);
    var scale = getRandom(0.3, 0.5);
    var rotationX = getRandom(-60, 60);
    var rotationY = getRandom(-180, 180);
    var position = getPolarPosition(distance, rotationX, rotationY);
    clone.x = position.x;
    clone.y = position.y;
    clone.z = position.z;
    clone.url = urls[i];
    setScale(clone, scale);
    clone.rotationY = rotationY;
    clone.addEventListener(MouseEvent.CLICK, onClick);
    scene.addChild(clone);
  }
}
function onClick(eventObject) {
  var mesh = eventObject.object;
  window.open(mesh.url);
}
function setScale(mesh, scale) {
  mesh.transform.scale = new Vector3D(scale, scale, scale);
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material = cube.material;
  material.texture = assets[0];
  view.render();
}
function rotate(timeStamp) {
  var camera = view.camera;
  angle += timeStamp / 2000;
  setCamera(camera, distance, angle);
  view.render();
}
function setCamera(camera, distance, angle) {
  camera.x = Math.cos(angle) * distance;
  camera.z = Math.sin(angle) * distance;
  camera.lookAt(ORIGIN);
}
function getRandom(min, max) {
  var random = Math.random() * (max - min) + min;
  return random;
}
function getPolarPosition(distance, rotationX, rotationY) {
  var vector = new Vector3D(distance, 0, 0);
  var matrix = new Matrix3D();
  matrix.appendRotation(rotationY, Vector3D.Y_AXIS);
  matrix.appendRotation(rotationX, Vector3D.X_AXIS);
  return matrix.transformVector(vector);
}

ロールオーバーとロールアウトでオブジェクトの大きさを変える

つぎに扱うマウスイベントは、ロールオーバーとロールアウトだ。ポインタの重なったオブジェクトは大きくして、マウスインタラクションを示したい。ロールオーバーとロールアウトは、それぞれ定数MouseEvent.MOUSE_OVERMouseEvent.MOUSE_OUTのイベントにリスナー関数を加えて扱う表1⁠。

オブジェクト.addEventListener(MouseEvent.MOUSE_OVER, リスナー関数)
オブジェクト.addEventListener(MouseEvent.MOUSE_OUT, リスナー関数)
表1 マウス操作を扱うMouseEventクラスのイベント
マウス操作イベント
クリックMouseEvent.CLICK
ロールオーバーMouseEvent.MOUSE_OVER
ロールアウトMouseEvent.MOUSE_OUT

立方体のオブジェクトそれぞれに3つのイベントリスナーを加えるので、そのための関数(setMouseListener())を別に定めることにしよう。引数は立体のオブジェクトに続けて、つぎのように3つのマウスイベントのリスナー関数を渡す。

setMouseListener(オブジェクト, ロールオーバー, ロールアウト, クリック)

オブジェクトにマウスポインタがロールオーバーしたら大きさを5割増し、ロールアウトでもとに戻るようにしたい。すると、前掲コード1をつぎのように書き替えればよさそうに思うかもしれない。しかし、これでは望む動きにはならない。

function createCube(width, height, depth, light) {

  // cube.addEventListener(MouseEvent.CLICK, onClick);
  setMouseListener(cube, onMouseOver, onMouseOut, onClick);

}

function setMouseListener(mesh, over, out, click) {
  mesh.addEventListener(MouseEvent.MOUSE_OVER, over);
  mesh.addEventListener(MouseEvent.MOUSE_OUT, out);
  mesh.addEventListener(MouseEvent.CLICK, click);
}
function onMouseOver(eventObject) {
  var mesh = eventObject.object;
  setScale(mesh, 1.5);
}
function onMouseOut(eventObject) {
  var mesh = eventObject.object;
  setScale(mesh, 1);
}

function setScale(mesh, scale) {
  mesh.transform.scale = new Vector3D(scale, scale, scale);
}

オブジェクトの大きさを定める関数(setScale())は、DisplayObject.transformプロパティから参照したTransformオブジェクトのTransform.scaleプロパティを操作している。このとき、定める大きさの比率(scale)は、複製もとにした真ん中の立体が1となる。つまり、1を与えれば真ん中の立体と同じ大きさになり、1.5はさらにその5割増しだ。

ランダムに定めたそれぞれのオブジェクトの大きさをもとにしようとするなら、その比率を覚えておかなければならない。そこで、オブジェクトの大きさは、つぎのようにふたつの関数に分けて扱うことにする。ひとつはオブジェクトに与えた大きさの比率をプロパティ(scale)に定める関数(setScale())だ。そして、さらにそこから呼び出すもうひとつの関数(changeScale())が、そのプロパティ値にもとづいて、オブジェクトの大きさを引数の比率に変える。

function cloneMesh(mesh, urls) {

  setScale(clone, scale);

}

function onMouseOver(eventObject) {

  changeScale(mesh, 1.5);
}
function onMouseOut(eventObject) {

  changeScale(mesh, 1);
}

function setScale(mesh, scale) {
  // mesh.transform.scale = new Vector3D(scale, scale, scale);
  mesh.scale = scale;
  changeScale(mesh, 1);
}
function changeScale(mesh, scale) {
  var _scale = mesh.scale * scale;
  mesh.transform.scale = new Vector3D(_scale, _scale, _scale);
}

これでランダムな大きさでちりばめた立方体のオブジェクトが、ロールオーバーすると大きくなり、ロールアウトでもとに戻る図2⁠。マウスインタラクションが示されるため、クリックへと導きやすくなった。

図2 マウスポインタがロールオーバーすると立方体は大きくなる
図2 マウスポインタがロールオーバーすると立方体は大きくなる 図2 マウスポインタがロールオーバーすると立方体は大きくなる

これでできたと思いきや、しばらく試していると気になるふるまいがある。マウスポインタを動かさずに、オブジェクトの方から重なってきたときには、大きさが変わらない。また、マウスをロールオーバーさせてオブジェクトが大きくなっても、ポインタをそのまま動かさずにオブジェクト側が外れると、大きさは戻らない。つまり、マウスを動かさないと、ロールオーバーやロールアウトのイベントが起こらないということだ。

これは、おそらくマウスポインタとオブジェクトとの重なりを調べる手間は、なるべく減らそうとしたことによる仕様だ。もし、オブジェクトは動かないコンテンツだったら、その方が負荷は軽い。だが、今回のお題はそれでは困る。その場合は、マウスが動かなくても重なりを調べさせるプロパティView.forceMouseMoveを、つぎのようにtrueに定めればよい。

function createView(width, height, backgroundColor) {

  var view = new View(defaultRenderer);

  view.forceMouseMove = true;

}

これで、クリックおよびロールオーバーとロールアウトのマウスインタラクションが加わった。ここまでのJavaScriptはコード2にまとめた。また、サンプル2をjsdo.itに掲げてある。次回はこのお題の締めとして、回り込むカメラもマウスの動きに合わせたい。

コード2 ロールオーバーしたオブジェクトの大きさが変わる
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var Matrix3D = require("awayjs-core/lib/geom/Matrix3D");
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 DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var MouseEvent = require("awayjs-display/lib/events/MouseEvent");  //
var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitiveCubePrefab = require("awayjs-display/lib/prefabs/PrimitiveCubePrefab");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var TriangleMethodMaterial = require("awayjs-methodmaterials/lib/TriangleMethodMaterial");
var view;
var cube;
var imageDiffuse = "assets/trinket_diffuse.jpg";
var timer;
var ORIGIN = new Vector3D();
var angle = -Math.PI / 2;
var distance = 1500;
var stageWidth = 240;
var stageHeight = 180;
var urls = [
  "http://gihyo.jp/design/serial/01/away3d-typescript",
  "http://gihyo.jp/design/serial/01/createjs",
  "http://typescript.away3d.com",
  "https://developer.mozilla.org/ja/docs/Web/JavaScript",
  "http://createjs.com/#!/Home"
];
function initialize() {
  var directionalLight = createDirectionalLight(0.5, 0xFFFFFF);
  view = createView(stageWidth, stageHeight, 0x0);
  cube = createCube(400, 400, 400, directionalLight);
  setCamera(view.camera, distance, angle);
  cube.url = "http://fumiononaka.com";
  view.scene.addChild(cube);
  cloneMesh(cube, urls);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(imageDiffuse));
  timer = new RequestAnimationFrame(rotate);
  timer.start();
  view.render();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer();
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  view.forceMouseMove = true;
  return view;
}
function createCube(width, height, depth, light) {
  var material = new TriangleMethodMaterial();
  var cube = new PrimitiveCubePrefab(width, height, depth, 1, 1, 1, false)
  .getNewObject();
  cube.material = material;
  setScale(cube, 1);
  setMouseListener(cube, onMouseOver, onMouseOut, onClick);
  material.lightPicker = new StaticLightPicker([light]);
  return cube;
}
function cloneMesh(mesh, urls) {
  var scene = view.scene;
  var count = urls.length;
  for (var i = 0; i < count; i++) {
    var clone = mesh.clone();
    var distance = getRandom(500, 1200);
    var scale = getRandom(0.3, 0.5);
    var rotationX = getRandom(-60, 60);
    var rotationY = getRandom(-180, 180);
    var position = getPolarPosition(distance, rotationX, rotationY);
    clone.x = position.x;
    clone.y = position.y;
    clone.z = position.z;
    clone.url = urls[i];
    setScale(clone, scale);
    clone.rotationY = rotationY;
    setMouseListener(clone, onMouseOver, onMouseOut, onClick);
    scene.addChild(clone);
  }
}
function setMouseListener(mesh, over, out, click) {
  mesh.addEventListener(MouseEvent.MOUSE_OVER, over);
  mesh.addEventListener(MouseEvent.MOUSE_OUT, out);
  mesh.addEventListener(MouseEvent.CLICK, click);
}
function onMouseOver(eventObject) {
  var mesh = eventObject.object;
  changeScale(mesh, 1.5);
}
function onMouseOut(eventObject) {
  var mesh = eventObject.object;
  changeScale(mesh, 1);
}
function onClick(eventObject) {
  var mesh = eventObject.object;
  window.open(mesh.url);
}
function setScale(mesh, scale) {
  mesh.scale = scale;
  changeScale(mesh, 1);
}
function changeScale(mesh, scale) {
  var _scale = mesh.scale * scale;
  mesh.transform.scale = new Vector3D(_scale, _scale, _scale);
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material = cube.material;
  material.texture = assets[0];
  view.render();
}
function rotate(timeStamp) {
  var camera = view.camera;
  angle += timeStamp / 2000;
  setCamera(camera, distance, angle);
  view.render();
}
function setCamera(camera, distance, angle) {
  camera.x = Math.cos(angle) * distance;
  camera.z = Math.sin(angle) * distance;
  camera.lookAt(ORIGIN);
}
function getRandom(min, max) {
  var random = Math.random() * (max - min) + min;
  return random;
}
function getPolarPosition(distance, rotationX, rotationY) {
  var vector = new Vector3D(distance, 0, 0);
  var matrix = new Matrix3D();
  matrix.appendRotation(rotationY, Vector3D.Y_AXIS);
  matrix.appendRotation(rotationX, Vector3D.X_AXIS);
  return matrix.transformVector(vector);
}
サンプル1 Away3D 14/11/05: Dealing with rollover, rollout and click mouse events

おすすめ記事

記事・ニュース一覧