function

Angular.jsと
three.jsを
一緒に使った時の話
SCRIPTY #1
2014/09/16(火)
@pirosikick
@pirosikick
ぴろしきっく
•
穴井宏幸
•
エンジニア
•
•
業務でのWD経験は1年くらいしかない
•
今はiOSアプリや、そのバックエンドの開発
JavaScript、HTML5
今日話すこと
•
Angular.jsとthree.jsでサービスを作った時の
•
うまくいったこと
•
あー失敗したわー
•
こうしとけばよかったー
•
学習しながらだったので、
•
「あーこういうやり方あるのかー」
•
ってのにあとで気づくことが多かった。。
今日話さないこと
•
Angular.jsの使い方
•
Three.jsの使い方
何作ったん?
•
easy-video-mapping.com
•
•
プロジェクションマッピングをブラウザで
社内ベンチャー制度で開発
•
http://japan.cnet.com/news/business/35034398/
https://vine.co/v/Mbnrgl05I33
管理画面は
Angular.js
作成画面
後で画像貼る
three.js(WebGL)
Angular.js
今日は作成画面の実装について話します。
<script
<script
<script
<script
!
規模感
•
中規模くらい
•
JSが9000行弱
•
•
右:<script>一覧
2人で開発
src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script>
src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular-animate.min.js"></script>
src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular-route.min.js"></script>
<!-- build:js /scripts/editor/vendor.js -->
<script src="/bower_components/threejs/build/three.min.js"></script>
<script src="/bower_components/modernizr/modernizr.js"></script>
<script src="/bower_components/spectrum/spectrum.js"></script>
<script src="/bower_components/qrcodejs/qrcode.min.js"></script>
<script src="/bower_components/lodash/dist/lodash.min.js"></script>
<script src="/bower_components/angular-underscore/angular-underscore.min.js"></script>
<!-- endbuild -->
!
!
<script src="https://skyway.io/dist/0.3/peer.js"></script>
<script src="/scripts/url.js"></script>
<script src="/scripts/getUserMedia.js"></script>
<!-- build:js /scripts/THREEx.js -->
<script src="/scripts/THREEx/THREEx.js"></script>
<script src="/scripts/THREEx/EffectPlayer.js"></script>
<script src="/scripts/THREEx/EffectGroup.js"></script>
<script src="/scripts/THREEx/effects/Effect.js"></script>
<script src="/scripts/THREEx/effects/FadeIn.js"></script>
<script src="/scripts/THREEx/effects/FadeOut.js"></script>
<script src="/scripts/THREEx/effects/FadeInOut.js"></script>
<script src="/scripts/THREEx/effects/ColorChanging.js"></script>
<script src="/scripts/THREEx/effects/LineTracing.js"></script>
<script src="/scripts/THREEx/geometries/CircleLineGeometry.js"></script>
<script src="/scripts/THREEx/geometries/TriangleGeometry.js"></script>
<script src="/scripts/THREEx/geometries/HexagonBoxGeometry.js"></script>
<script src="/scripts/THREEx/controls/Control.js"></script>
<script src="/scripts/THREEx/controllers/Controller.js"></script>
<script src="/scripts/THREEx/controllers/PlaneController.js"></script>
<script src="/scripts/THREEx/controllers/CylinderSurfaceController.js"></script>
<script src="/scripts/THREEx/controllers/LineController.js"></script>
<script src="/scripts/THREEx/controllers/HexagonController.js"></script>
<script src="/scripts/THREEx/objects/BaseObject.js"></script>
<script src="/scripts/THREEx/objects/Line.js"></script>
<script src="/scripts/THREEx/objects/Plane.js"></script>
<script src="/scripts/THREEx/objects/Triangle.js"></script>
<script src="/scripts/THREEx/objects/Circle.js"></script>
<script src="/scripts/THREEx/objects/Cylinder.js"></script>
<script src="/scripts/THREEx/objects/CylinderSurface.js"></script>
<script src="/scripts/THREEx/objects/RemoteCamera.js"></script>
<script src="/scripts/THREEx/objects/Hexagon.js"></script>
<script src="/scripts/THREEx/textures/Texture.js"></script>
<script src="/scripts/THREEx/textures/RemoteTexture.js"></script>
<script src="/scripts/THREEx/Editor.js"></script>
<!-- endbuild -->
!
<!-- build:js /scripts/editor/app.js -->
<script src="/scripts/clipper/clipper.js"></script>
<script src="/scripts/ttg/ttg.js"></script>
<script src="/scripts/editor/app.js"></script>
<script src="/scripts/editor/controllers/NewArtworkCtrl.js"></script>
<script src="/scripts/editor/controllers/ArtworkCtrl.js"></script>
<script src="/scripts/editor/controllers/MainCtrl.js"></script>
<script src="/scripts/editor/controllers/SideMenuCtrl.js"></script>
<script src="/scripts/editor/controllers/TextureMenuCtrl.js"></script>
<script src="/scripts/editor/controllers/ObjectTabCtrl.js"></script>
<script src="/scripts/editor/controllers/EffectTabCtrl.js"></script>
<script src="/scripts/editor/controllers/SettingTabCtrl.js"></script>
<script src="/scripts/editor/controllers/UploadTextureCtrl.js"></script>
<script src="/scripts/editor/controllers/WebcamTextureCtrl.js"></script>
<script src="/scripts/editor/controllers/RemoteTextureCtrl.js"></script>
<script src="/scripts/editor/controllers/TextTextureCtrl.js"></script>
<script src="/scripts/editor/controllers/ErrorCtrl.js"></script>
<script src="/scripts/editor/services/editor.js"></script>
<script src="/scripts/editor/services/textureManager.js"></script>
<script src="/scripts/editor/services/URL.js"></script>
<script src="/scripts/editor/services/webcam.js"></script>
<script src="/scripts/editor/services/Effect.js"></script>
<script src="/scripts/editor/services/effectPlayer.js"></script>
<script src="/scripts/editor/services/objectCounter.js"></script>
<script src="/scripts/editor/services/getWithUUID.js"></script>
<script src="/scripts/editor/services/peer.js"></script>
<script src="/scripts/editor/services/qrcode.js"></script>
<script src="/scripts/editor/services/THREE.js"></script>
<script src="/scripts/editor/directives/texture.js"></script>
<script src="/scripts/editor/directives/spectrum.js"></script>
<!-- endbuild -->
!
<script src="/scripts/editor/templates.js"></script>
<script src="/scripts/clipper/templates.js"></script>
最初はthree.jsのみ
•
•
HackDayで開発
•
社内Hackathon、24時間で開発、90秒で発表
•
参考記事)弊社テックブログ
24時間しか無いので「動けばいいや」精神で開発
•
1ファイル
•
グローバル変数・init関数・render関数
グローバル変数に定義
!
var camera, scene, renderer, domElement;
var addButton = document.getElementById('add-button');
初期化関数 init
// 初期化関数
function init () {
// カメラ、シーン、レンダラーの初期化
camera = new THREE.PerspectiveCemaera(...);
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer(...);
!
}
// イベント設定
domElement.addEventListener('mousedown', function (e) {...}, false);
addButton.addEventListener('click', function (e) {
scene.add( new THREE.Mesh(...) );
}, false);
描画関数 render
!
(function render () {
renderer.render(camera, scene);
!
window.requestAnimationFrame(render);
})();
次にClass化
•
本格的にサービスとして取り掛かり始めた時期
•
コードの見通しを良くしたかった
•
今見るとそんなに良くなってなくてワロタ
function Editor (options) {
this.camera = new THREE.PerspectiveCamera(...);
this.scene = new THREE.Scene();
this.renderer = new THREE.WebGLRenderer(...);
// その他、イベントの初期化関数の呼び出しなど。。。
}
!
Editor.prototype = {
constructor: Editor,
// イベントの初期化関数とか
// アニメーション開始
startAnimation: function () {
var renderer = this.renderer;
var camera = this.camera;
var scene this.scene;
}
}
(function loop () {
renderer.render(camera, scene);
window.requestAnimationFrame(loop);
})();
Angular.js導入
•
•
編集機能が増えてきたので
•
オブジェクトの追加・削除・ソート
•
色、不透明度
•
テクスチャの管理 etc
Class化したものをfactoryでラップ
•
Three側のイベントは$rootScope.$broadcastで通知
Classをfactoryでラップ
実際はもっと長い
angular.module('App')
.factory('editor', ['$window', ‘$rootScope',
function ($window, $rootScope) {
var editor = new $window.Editor(...); ←Class化したやつ
// THREE.jsのイベントを伝搬
// ほとんどがただ$broadcastに渡すだけ。。
editor.addEventListener('event-from-three', function (e) {
$rootScope.$broadcast('event-name', { object: e.object, ... });
});
// factory, serviceはシングルトンなので
// アニメーション開始してしまう
editor.startAnimation();
}
]);
// ラップしたオブジェクトを返す
return {
domElement: angular.element( editor.renderer.domElement ),
addObject: function (object) { ... },
removeObject: function (object) { ... }
}
factory,serviceで
ラップする
×
•
無駄なコードが多い
•
イベントをそのまま$broardcastするだけの記述
•
Classを修正→factory側も同じ修正で二度手間
•
factoryがどんどん肥大化する
•
Three.jsのObject,Textureとかもラップしてて、
•
各々どんどん肥大化した
解決案
Classを捨ててservice内に直接書く
.service('editor', ['$window', 'THREE', function ($window, THREE) {
// Editor.prototype.initとほぼ同じ
this.init = function init (domElement) {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(・・・);
this.renderer = new THREE.WebGLRenderer({ domElement: domElement })
// もろもろのイベント処理
startAnimation(this.renderer, this.camera, this.scene);
};
// アニメーションを開始する関数
function startAnimation(renderer, camera, scene) {
(function loop () {
renderer.render(camera, scene);
$window.requestAnimationFrame(loop);
})();
}
}]);
解決案
three.jsのクラスもなるべくそのまま使う
window.THREEをconstantで定義
(valueでも可)
!
angular.module('App').constant('THREE', THREE);
DIして直接使う
angular.module('App')
.controller('Ctrl', function ($scope, THREE, editor) {
!
// オブジェクトの追加
$scope.add = function () {
editor.scene.add( new THREE.Mesh(...) );
}
!
});
逆によかった
service, factory,
provider
webcam(factory)
•
navigator.getUserMediaのラッパー
•
Webカメラに簡単にアクセスできる
•
今思うとserviceで良かった気がする
だいたいこういう感じの実装
angular.module('App')
.service('webcam', ['$window', '$q', function ($window, $q) {
!
var navigator = $window.navigator;
!
navigator._getUserMedia = (navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.oGetUserMedia ||
navigator.msGetUserMedia);
!
// navigator.getUserMediaをラップ
this.openStream = function (options) {
var defer = $q.defer();
var resolve = defer.resolve.bind(defer);
var reject = defer.reject.bind(defer);
!
navigator._getUserMedia(options, resolve, reject);
!
return defer.promise;
}
!
}]);
controllerで取得したstreamをURLに変換し
$scopeに入れる
angular.module('App')
.controller('Ctrl', ['webcam', '$scope', function (webcam, $scope) {
!
$scope.webcamURL = '';
!
webcam.openStream({ video: true, audio: true })
.then(function (mediaStream) {
!
$scope.webcamURL = $window.URL.createObjectURL(mediaStream);
!
});
!
}]);
URLが$scopeに代入されたタイミングで再生が始まる
!
<video ng-if="webcamURL" ng-src="{{ webcamURL }}" autoplay></video>
peer(provider)
•
peer.jsのラップ
•
WebRTC P2P通信のライブラリ
•
他デバイスからの映像をTextureとして使用
•
skyway使っていたので、
•
アプリケーションキーを渡すためにproviderで定義
peer(provider)
configブロックで鍵を渡す
angular.module('App')
.config(['peerProvider', function (peerProvider) {
!
peerProvider.apiKey = 'skywayのAPIキー';
!
}]);
Skywayへの接続
angular.module('...')
.controller('...', ['peer', function (peer) {
!
peer.create()
// skywayと接続が確立されると
// idが発行される
.then(function (id) {
!
// このidを接続先に渡す。
// 本サービスではQRコードで。
console.log(id);
!
});
P2Pで接続要求が来た時
// $rootScope.$broadcastでイベントが流れてくる
$scope.$on('peer:call', function ($e, id, mediaConnection) {
!
mediaConnection.answer()
.then(function (stream) {
// 相手のウェブカメラのURL
$scope.videoURL = window.URL.createObjectURL(steram);
});
!
});
<video ng-if="videoURL" ng-src="{{ videoURL }}" />
directiveも
何個か作った
•
独自のHTMLタグや属性を作れる仕組み
•
共通のDOMやそれに紐づく処理をまとめるのに便利
•
既存のdirective(ng-showなど)とも組み合わせる
ことができるので便利
例
<input spectrum="…">
←これ
<input type="color"
ng-model="backgroundColor"
spectrum="{ showInput: true }">
・spectrum(jQueryプラグイン)のラップ
・オプションをオブジェクトで渡せる
・input type=colorが実装されているブラウザではそれを使う
<ttg-canvas>
「文字」のテクスチャを作るcanvasのDirective
Text Texture Generatorの略
実際にはこういうタグ
<!-- attributeでデータの受け渡し -->
<ttg-canvas name="add-text"
text="text"
font-size="fontSize"
text-color="textColor"
background-color="bgColor"
text-align="textAlign"
autosize="true"></ttg-canvas>
!
<!-- inputを変更するとcanvasの内容が更新 -->
<p>
<span>font size</span>
<input ng-model="fontSize" type="number" max="50" min="5" step="1">
</p>
<p>
<span>text color:</span>
<input ng-model="textColor" type="color" spectrum>
</p>
<p>
<span>background color:</span>
<input ng-model="bgColor" type="color" spectrum>
</p>
テクスチャ画像はservice経由で取得する
!
angular.module('App')
.controller('Ctrl', function ($scope, $ttg, THREE) {
!
$scope.generateTextTexture = function () {
!
// serviceから画像の取得
var imageURL = $ttg('add-text').generate();
!
}
!
});
// Textureの生成・設定
var texture = THREE.ImageUtils.loadTexture(imageURL);
$scope.object.material.map = texture;
×
不要なDirective化
•
•
工数かけてDirectiveにする価値があったか
•
2回しか使わない・汎用性低い
•
ttgとかわかりづらいし
なんでもDirectiveにしたくなる「Directive中毒」
•
もっと難しいDirectiveを作りたい欲の高まり
Directive
よく考えて実装しよう
•
本当に使い回すのか?
•
逆に複雑になってしまってないか?
•
OSSでもう同じやつ誰か実装してないか?
まとめ
もし大規模的に使うなら
ある程度勉強してからの方がいい
•
Angularの機能の使い分けを理解する
•
service or factory or provider?
•
run or config? etc
•
ラップとか余計な抽象化しない
•
Directiveなら ng-* 系の
本家のを見ると結構わかるようになる
相性はよかったと思う
•
大規模開発向きで構造的に書けるのがよかった
•
inputを多用するサービスだったので相性よかった
•
•
メリットを多く感じられたのだと思う
今は他の選択肢もあるけど
•
次同じような開発するなら、Vue.js
•
簡単なGUI編集なら、dat.GUI
thanks:)