サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

EaselJSのMatrix2Dクラスを使ったインスタンスの変形

ID: FN1210005 Technique: HTML5 and JavaScript

Matrix2Dクラスは、2次元平面の座標を数学の行列により変換します。Matrix2Dクラスの変換行列を使うと、インスタンスに移動や伸縮、回転、傾斜などの変形を組合わせて加えることができます。「EaselJSを使ったインスタンスの傾斜・伸縮・移動」と同じような変形を、今度はMatrix2Dクラスという別の道具で試してみましょう。


01 読込んだ画像の3つの角にハンドルを置く

前出「EaselJSを使ったインスタンスの傾斜・伸縮・移動」のコード001では、画像の右上角に置いたハンドルでインスタンスを傾斜および伸縮させました。本稿ではハンドルを3つに増やして、Matrix2Dクラスによりインスタンスの座標変換を行います(図001)[*1]

図001■3つのハンドルをドラッグすると画像が変形する
図001左   図001右

まず、PNGファイル(image.png)はHTMLドキュメントと同じ場所のフォルダに(images)納め、PreloadJSクラスを用いて読込み、Bitmapインスタンスに納めてCanvasの真ん中に置きます(図002)。この処理は、前出「EaselJSを使ったインスタンスの傾斜・伸縮・移動」のコード001と変わりません。書上げたscript要素全体は、後にコード001として掲げます。抜書きして示すステートメントの行番号は、後掲コード001にもとづきます。

  1. var stage;
  2. var myBitmap;
  3. var file = "images/image.png";
  1. function initialize() {
  2.   var canvasElement = document.getElementById("myCanvas");
  3.   stage = new Stage(canvasElement);
  4.   var loader = new PreloadJS(false);
  5.   loader.onFileLoad = draw;
  6.   loader.loadFile(file);
  7.   myBitmap = new Bitmap(file);
  8.   setAppearance(myBitmap, canvasElement.width / 2, canvasElement.height / 2);
  1. }
  2. function setAppearance(instance, nX, nY) {
  3.   instance.x = nX;
  4.   instance.y = nY;
  5.   stage.addChild(instance);
  6. }
  7. function draw(eventObject) {
  8.   var myImage = eventObject.result;
  9.   var imageWidth = myImage.width;
  10.   var imageHeight = myImage.height;
  11.   myBitmap.x -= imageWidth / 2;
  12.   myBitmap.y -= imageHeight / 2;
  1.   stage.update();
  2. }

図002■画像ファイルを読込んでCanvasの真ん中に置く
図002

つぎに、ドラッグするハンドルのShapeインスタンスに円を描いて、画像が納められたBitmapインスタンスの3つの角に置きます(図003)。つくるハンドルの数が3つに増えて、左上と右上および右下に置いたこと(第32〜34行目および第47〜49行目)を除けば、基本的に前出「EaselJSを使ったインスタンスの傾斜・伸縮・移動」のコード001と中身は変わりません。いよいよつぎの項から、Matrix2Dクラスの変換行列を用いたインスタンスの変形についてご説明します。

  1. var handleSettings = {radius:5, color:"blue"};
  2. var handles = [];
  3. function initialize() {
  1.   for (var i = 0; i < 3; i++) {
  2.     handles[i] = createHandle(handleSettings);
  3.   }
  4. }
  1. function draw(eventObject) {
  2.   var myImage = eventObject.result;
  3.   var imageWidth = myImage.width;
  4.   var imageHeight = myImage.height;
  1.   setAppearance(handles[0], myBitmap.x + imageWidth, myBitmap.y);
  2.   setAppearance(handles[1], myBitmap.x, myBitmap.y);
  3.   setAppearance(handles[2], myBitmap.x, myBitmap.y + imageHeight);
  4.   stage.update();
  5. }
  6. function createHandle(settings) {
  7.   var myShape = new Shape();
  8.   myShape.onPress = startDrag;
  9.   drawHandle(myShape.graphics, settings);
  10.   return myShape;
  11. }
  12. function drawHandle(myGraphics, settings) {
  13.   myGraphics.beginFill(settings.color);
  14.   myGraphics.drawCircle(0, 0, settings.radius);
  15. }
  16. function startDrag(eventObject) {
  17.   eventObject.instance = this;
  18.   eventObject.onMouseMove = drag;
  19.   eventObject.onMouseUp = stopDrag;
  20.   this.offset = new Point(this.x - eventObject.stageX, this.y - eventObject.stageY);
  21. }
  22. function drag(eventObject) {
  23.   var instance = this.instance;
  24.   var offset = instance.offset;
  25.   instance.x = eventObject.stageX + offset.x;
  26.   instance.y = eventObject.stageY + offset.y;
  1.   stage.update();
  2. }
  3. function stopDrag(eventObject) {
  4.   this.onMouseMove = this.onMouseUp = null;
  5. }

図003■ドラッグするハンドルのインスタンスを画像の3つの角に置く
図010左

[*1] 傾斜は、インスタンスの矩形の境界領域を平行四辺形に変形します。つまり、向かい合う2辺は平行のままで、台形に変えることはできません。いわゆる「パースペクティブ」はかけられないのです。そのため、インスタンスの3つの角の座標が定まれば、残りの4つ目の位置は決まります。


02 Matrix2Dクラスの変換行列

「行列」はテーブル(表)のように縦横に並べた数値を括弧([]または())にくくって表し、行列の中の各数値は「成分」と呼ばれます。また、行数と列数が等しい行列を「正方行列」といいます。Matrix2Dクラスの変換行列は、以下のような3行×3列の正方行列です。ただし、第3行目の[0 0 1]は行列計算の便宜上加えられたもので、つねにこの値に決まっています。つまり、実質は2行分の6成分からなります。

  a     c     tx  
  b     d     ty  
  0     0     1  

これら6成分の値を変えることにより、平行移動や伸縮、回転、傾斜などの座標変換ができます(表001)。これらの中から今回のお題に使う変換は、平行移動と伸縮および傾斜です。したがって、変換行列のa、b、c、d、tx、tyの6成分をすべて計算して定めます。

001■変換行列のプロパティ値とその変換結果
変換 関連するDisplayObject
のプロパティ
変換行列の値 変換結果
デフォルト
  1     0     0  
  0     1     0  
  0     0     1  
平行移動 x
y
  1     0     ピクセル数x  
  0     1     ピクセル数y  
  0     0     1  
伸縮 scaleX
scaleY
  伸縮率x     0     0  
  0     伸縮率y     0  
  0     0     1  
回転 rotation
  cos回転角     -sin回転角     0  
  sin回転角     cos回転角     0  
  0     0     1  
傾斜 skewX
skewY
  1     傾斜率x     0  
  傾斜率y     1     0  
  0     0     1  

6つの成分の計算のうち、平行移動と伸縮はDisplayObjectクラスのプロパティ(x、y、scaleX、scaleY)の定め方と同じなのでご説明はいらないでしょう。傾斜は、プロパティ(scaleXとscaleY)が角度で定められるのに対して、成分値の傾斜率は伸縮率と同じように比率で決まります。少しわかりにくいかもしれません。けれど、6成分の計算式を並べてみると、きれいな関係があることに気づくでしょう。

a: 水平伸縮値 / デフォルトの幅
b: 垂直傾斜値 / デフォルトの幅
c: 水平傾斜値 / デフォルトの高さ
d: 垂直伸縮値 / デフォルトの高さ
tx: 水平座標値
ty: 垂直座標値

図004のように3つのハンドルの座標を定め、元イメージから定まるデフォルトの幅と高さをそれぞれwおよびhとおけば、3点の座標から各成分値がつぎのように導かれます。

図004■3つのハンドルの座標に合わせて変換行列の成分を計算する
図004

a = (x0 - x1) / w
b = (y0 - y1) / w
c = (x2 - x1) / h
d = (y2 - y1) / h
tx = x1
ty = y1

03 Matrix2Dクラスによりインスタンスを座標変換する

それでは、Matrix2Dクラスにより、インスタンスの座標をどう変換するかです。まず、Matrix2D()コンストラクタには、6成分を引数に渡します。もっとも、6成分は同じ名前(abcdtxty)でMatrix2Dオブジェクトのプロパティに備わっていますから、後でそれぞれの値を与えても構いません。コンストラクタの引数を省くと、デフォルトの単位行列のMatrix2Dオブジェクトがつくられます。

new Matrix2D(a, b, c, d, tx, ty)

Matrix2Dは数学的な変換行列のデータとそれを操作するプロパティやメソッドが備えられているオブジェクトで、Canvasに表示するDisplayObjectインスタンスとは切離されており、インスタンスを直ちに変形する訳ではありません。変形するインスタンスにMatrix2Dオブジェクトの変換行列を適用するのがMatrix2D.decompose()メソッドです。

Matrix2Dオブジェクト.decompose(変形するオブジェクト)

Matrix2D.decompose()メソッドは、Matrix2Dオブジェクトから変換のためのプロパティ値を導いて、引数のインスタンスのプロパティに与えます。変換のためのプロパティとは、xyscaleXscaleYskewXskewYの6つです。つまり、平行移動と伸縮および傾斜(回転も含む)が一度に行えるのです。

Matrix2Dクラスのこのふたつのメソッドを使うと、インスタンスがつぎのようにして変形できます。インスタンスを変形する元締めの関数(transformBitmap())が、3つのハンドルの座標を調べて、本稿のお題の主菜となる座標変換の関数(transform())を呼出します(第80〜83行目)。座標変換の関数は、引数に受取った3座標から前述の計算式で6成分値を定め(第95〜101行目)、Matrix2D.decompose()メソッドによりインスタンスを変形しています(第102行目)。

  1. function transformBitmap() {
  2.   var point0 = getHandlePoint(0);
  3.   var point1 = getHandlePoint(1);
  4.   var point2 = getHandlePoint(2);
  5.   transform(point0, point1, point2);
  6. }
  7. function getHandlePoint(handleId) {
  8.   var handle = handles[handleId];
  9.   return new Point(handle.x, handle.y);
  10. }
  11. function transform(point0, point1, point2) {
  12.   var myImage = myBitmap.image;
  13.   var point1_0_x = point0.x - point1.x;
  14.   var point1_0_y = point0.y - point1.y;
  15.   var point1_2_x = point2.x - point1.x;
  16.   var point1_2_y = point2.y - point1.y;
  17.   var matrix = new Matrix2D();
  18.   matrix.tx = point1.x;
  19.   matrix.ty = point1.y;
  20.   matrix.a = point1_0_x / myImage.width;
  21.   matrix.b = point1_0_y / myImage.width;
  22.   matrix.c = point1_2_x / myImage.height;
  23.   matrix.d = point1_2_y / myImage.height;
  24.   matrix.decompose(myBitmap);
  25.   stage.update();
  26. }

これでインスタンスは、ドラッグする3つのハンドルに合わせて、Matrix2Dクラスの変換行列により変形されます(図005)。書上げたscript要素全体は、以下のコード001のとおりです。

図005■インスタンスは3つのハンドルに合わせて変形する
図005

コード001■ハンドル3つのドラッグに合わせてインスタンスを座標変換する
  1. <script>
  2. var createjs = window;
  3. </script>
  4. <script src="easeljs/utils/UID.js"></script>
  5. <script src="easeljs/geom/Matrix2D.js"></script>
  6. <script src="easeljs/geom/Point.js"></script>
  7. <script src="easeljs/events/MouseEvent.js"></script>
  8. <script src="easeljs/display/DisplayObject.js"></script>
  9. <script src="easeljs/display/Container.js"></script>
  10. <script src="easeljs/display/Stage.js"></script>
  11. <script src="easeljs/display/Bitmap.js"></script>
  12. <script src="easeljs/display/Shape.js"></script>
  13. <script src="easeljs/display/Graphics.js"></script>
  14. <script src="preloadjs/AbstractLoader.js"></script>
  15. <script src="preloadjs/PreloadJS.js"></script>
  16. <script src="preloadjs/TagLoader.js"></script>
  17. <script src="preloadjs/XHRLoader.js"></script>
  18. <script>
  19. var stage;
  20. var myBitmap;
  21. var file = "images/image.png";
  22. var handleSettings = {radius:5, color:"blue"};
  23. var handles = [];
  24. function initialize() {
  25.   var canvasElement = document.getElementById("myCanvas");
  26.   stage = new Stage(canvasElement);
  27.   var loader = new PreloadJS(false);
  28.   loader.onFileLoad = draw;
  29.   loader.loadFile(file);
  30.   myBitmap = new Bitmap(file);
  31.   setAppearance(myBitmap, canvasElement.width / 2, canvasElement.height / 2);
  32.   for (var i = 0; i < 3; i++) {
  33.     handles[i] = createHandle(handleSettings);
  34.   }
  35. }
  36. function setAppearance(instance, nX, nY) {
  37.   instance.x = nX;
  38.   instance.y = nY;
  39.   stage.addChild(instance);
  40. }
  41. function draw(eventObject) {
  42.   var myImage = eventObject.result;
  43.   var imageWidth = myImage.width;
  44.   var imageHeight = myImage.height;
  45.   myBitmap.x -= imageWidth / 2;
  46.   myBitmap.y -= imageHeight / 2;
  47.   setAppearance(handles[0], myBitmap.x + imageWidth, myBitmap.y);
  48.   setAppearance(handles[1], myBitmap.x, myBitmap.y);
  49.   setAppearance(handles[2], myBitmap.x, myBitmap.y + imageHeight);
  50.   stage.update();
  51. }
  52. function createHandle(settings) {
  53.   var myShape = new Shape();
  54.   myShape.onPress = startDrag;
  55.   drawHandle(myShape.graphics, settings);
  56.   return myShape;
  57. }
  58. function drawHandle(myGraphics, settings) {
  59.   myGraphics.beginFill(settings.color);
  60.   myGraphics.drawCircle(0, 0, settings.radius);
  61. }
  62. function startDrag(eventObject) {
  63.   eventObject.instance = this;
  64.   eventObject.onMouseMove = drag;
  65.   eventObject.onMouseUp = stopDrag;
  66.   this.offset = new Point(this.x - eventObject.stageX, this.y - eventObject.stageY);
  67. }
  68. function drag(eventObject) {
  69.   var instance = this.instance;
  70.   var offset = instance.offset;
  71.   instance.x = eventObject.stageX + offset.x;
  72.   instance.y = eventObject.stageY + offset.y;
  73.   transformBitmap();
  74.   stage.update();
  75. }
  76. function stopDrag(eventObject) {
  77.   this.onMouseMove = this.onMouseUp = null;
  78. }
  79. function transformBitmap() {
  80.   var point0 = getHandlePoint(0);
  81.   var point1 = getHandlePoint(1);
  82.   var point2 = getHandlePoint(2);
  83.   transform(point0, point1, point2);
  84. }
  85. function getHandlePoint(handleId) {
  86.   var handle = handles[handleId];
  87.   return new Point(handle.x, handle.y);
  88. }
  89. function transform(point0, point1, point2) {
  90.   var myImage = myBitmap.image;
  91.   var point1_0_x = point0.x - point1.x;
  92.   var point1_0_y = point0.y - point1.y;
  93.   var point1_2_x = point2.x - point1.x;
  94.   var point1_2_y = point2.y - point1.y;
  95.   var matrix = new Matrix2D();
  96.   matrix.tx = point1.x;
  97.   matrix.ty = point1.y;
  98.   matrix.a = point1_0_x / myImage.width;
  99.   matrix.b = point1_0_y / myImage.width;
  100.   matrix.c = point1_2_x / myImage.height;
  101.   matrix.d = point1_2_y / myImage.height;
  102.   matrix.decompose(myBitmap);
  103.   stage.update();
  104. }
  105. </script>

[*2] 前出「EaselJSを使ったインスタンスの傾斜・伸縮・移動」の考え方に沿って、3つのハンドルの座標からDisplayObjectクラスのプロパティxyscaleXscaleYskewXskewYを定めて変形したサンプルがあります。それぞれの変換のための関数(transform())を互いに比べてみるとよいでしょう。



作成者: 野中文雄
作成日: 2012年10月18日


Copyright © 2001-2012 Fumio Nonaka.  All rights reserved.