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:)
© Copyright 2024 ExpyDoc