AngularCLIでng new ~~~ってコマンドを打つと、デフォルトで表示されるページがあるじゃないですか。
↓のこれです。
なんか華が足りない気がしませんか?そもそも私日本人だし、「Tour of Heroes」だの「CLI Documentation」だの言われてもすぐには理解できない。
ガイドが必要だ。華のあるガイドが。華といえばVTuberかな。VTuberよく知らんけど、3Dで作るんだろ?
こんな思考回路で、AngularCLIのデフォルトページにJavascript用3dライブラリThree.jsで作ったガイドを表示させ、各リンクの説明をさせることに致しました。
ここからは、ガイドをAngularWebページに表示させるよう格闘した今回の手順をざっくり解説していきます。
(Angular CLIなどは使えるものとして話していきます)
Angularプロジェクト新規作成~Three.jsのオブジェクト表示まで
まずはAngularCLIでプロジェクトを作ります。
今回のプロジェクト名は「AngularThreejs」にしましょう。
ng new AngularThreejsと入力してね。
できたら、新規コンポーネントとサービスを作りましょう。
ng generate component engineと入力し、作り終わったら
cd engine で作成したコンポーネントのフォルダに移動し
ng generate service engineと入力してね。
↑この画像はengineフォルダに移動せずサービスを作成しているので、ダメな例です。
では、実際に作っていきましょう。
今回弄るファイルは全部で6つ!
・styles.scss
・app.component.html
・app.component.ts
・engine.component.html
・engine.component.ts
・engine.service.ts
まず、app.component.htmlの末尾にengineを呼び出す処理を書きましょう。
ガイドを作る処理はengineに書いていきます。
<app-engine></app-engine>
engine.component.htmlにはcanvasタグを書いときましょう。
<div class="engine-wrapper">
<canvas #rendererCanvas></canvas>
</div>
ここでnpm install threeを実行しておきましょう。
これをやらないとthree.js使えないですからね。
engine.component.tsにはこう書きましょう。
サービス呼んだりしています。
import { EngineService } from './engine.service';
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-engine',
templateUrl: './engine.component.html',
styleUrls: ['./engine.component.scss']
})
export class EngineComponent implements OnInit {
@ViewChild('rendererCanvas', {static: true})
public rendererCanvas: ElementRef<HTMLCanvasElement>;
constructor(private engServ: EngineService) { }
ngOnInit() {
this.engServ.createScene(this.rendererCanvas);
this.engServ.animate();
}
}
engine-wrapperの定義はscssファイルに書きます。
z-indexは重要です。これを0にすると、uiより後ろにいかずリンクがクリックできません。
.engine-wrapper{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
#renderCanvas {
width : 100%;
height : 100%;
touch-action: none;
}
#renderCanvas:focus {
outline: none;
}
}
ではいよいよ、サービスを書いていきますが…面倒なんで、ソースを先に貼り、掻い摘んで解説しますね。
import * as THREE from 'three';
import { Injectable, ElementRef, OnDestroy, NgZone } from '@angular/core';
import { Vector3, Object3D, Font } from 'three';
@Injectable({
providedIn: 'root'
})
export class EngineService implements OnDestroy {
private canvas: HTMLCanvasElement;
private renderer: THREE.WebGLRenderer;
private camera: THREE.PerspectiveCamera;
private scene: THREE.Scene;
private light: THREE.AmbientLight;
private lightDirec: THREE.DirectionalLight;
private cubeHead: THREE.Mesh;
private cubeLeftHand: THREE.Mesh;
private cubeRightHand: THREE.Mesh;
private cone: THREE.Mesh;
private objGroup: THREE.Group;
private isRotate: boolean;
private frameId: number = null;
public constructor(private ngZone: NgZone) {}
public ngOnDestroy() {
if (this.frameId != null) {
cancelAnimationFrame(this.frameId);
}
}
createScene(canvas: ElementRef<HTMLCanvasElement>): void {
// The first step is to get the reference of the canvas element from our HTML document
this.canvas = canvas.nativeElement;
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
alpha: true, // transparent background
antialias: true // smooth edges
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
// create the scene
this.scene = new THREE.Scene();
this.objGroup = new THREE.Group();
// カメラの配置
this.camera = new THREE.PerspectiveCamera(
75, window.innerWidth / window.innerHeight, 0.1, 1000
);
this.camera.position.z = 5;
this.scene.add(this.camera);
// 環境光源
this.light = new THREE.AmbientLight( 0x404040 );
this.light.position.z = 10;
this.scene.add(this.light);
// 平行光源
this.lightDirec = new THREE.DirectionalLight( 0xFFFFFF, 1);
this.lightDirec.position.set(0, 10, 10);
this.scene.add(this.lightDirec);
// 頭?
const geometry = new THREE.BoxGeometry(1, 1, 1);
const loader = new THREE.TextureLoader();
const texture = loader.load('assets/img/tex1.png');
const material = new THREE.MeshStandardMaterial({ map: texture });
this.cubeHead = new THREE.Mesh( geometry, material );
this.cubeHead.position.x = 2;
// 左手?
const geoLeftHand = new THREE.BoxGeometry(0.6, 0.1, 0.1);
const matLeftHand = new THREE.MeshStandardMaterial({ color: 0x004f00 });
this.cubeLeftHand = new THREE.Mesh(geoLeftHand, matLeftHand);
this.cubeLeftHand.position.x = 1.3;
// 右手?
const geoRightHand = new THREE.BoxGeometry(0.6, 0.1, 0.1);
const matRightHand = new THREE.MeshStandardMaterial({ color: 0x004f00 });
this.cubeRightHand = new THREE.Mesh(geoRightHand, matRightHand);
this.cubeRightHand.position.x = 2.8;
// 胴体?
const geoCone = new THREE.ConeGeometry(1, 2, 8);
const matCone = new THREE.MeshStandardMaterial({ color: 0x774f77 });
this.cone = new THREE.Mesh(geoCone, matCone);
this.cone.position.x = 2;
this.cone.position.y = -1;
// 頭・左手・右手のグループ化
this.objGroup.add(this.cubeHead, this.cubeLeftHand, this.cubeRightHand, this.cone);
// シーン追加
this.scene.add(this.objGroup);
}
animate(): void {
// We have to run this outside angular zones,
// because it could trigger heavy changeDetection cycles.
this.ngZone.runOutsideAngular(() => {
window.addEventListener('DOMContentLoaded', () => {
this.render();
});
window.addEventListener('resize', () => {
this.resize();
});
});
}
render() {
this.frameId = requestAnimationFrame(() => {
this.render();
});
// VTuberを回す
if (this.isRotate) {
this.objGroup.rotation.x += 0.05;
}
this.renderer.render(this.scene, this.camera);
}
resize() {
const width = window.innerWidth;
const height = window.innerHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize( width, height );
}
rotate() {
// 回転オン
this.isRotate = true;
}
rotateOff() {
// 回転オフ
this.isRotate = false;
}
}
ガイドを作るべく、THREE.~~~Geometryってやつでオブジェクトを生成しています。
頭や手、胴体などを作っていますね。それにマテリアルを貼り付け、メッシュを生成しているわけです。
この頭、手、胴体はアニメーションの際にグループ化して動かしたいので、ojbGroupに追加してます。
isRotateがTrueの時、objGroupのrotation値を弄ることでobjGroupに追加したメッシュが同時に動いてくれるというわけです。
isRotateのオンオフを切り替えるrorate(),rotateOff()はapp.component.tsで呼び出します。
import { Component } from '@angular/core';
import { EngineService } from './engine/engine.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(private engServ: EngineService) { }
title = '令和';
explain = '';
private onButtonIn(mode: number) {
switch (mode) {
case 0:
this.engServ.rotate();
this.explain = 'Angular公式のチュートリアルナリよ~!';
break;
case 1:
this.engServ.rotate();
this.explain = 'Angular CLIのドキュメントナリよ~!';
break;
case 2:
this.engServ.rotate();
this.explain = 'Angular開発者達のブログなりよ~!(多分)';
break;
}
}
private onButtonOut() {
this.engServ.rotateOff();
this.explain = '';
}
}
最期に、このonButtonInとonButtonOutを呼び出すイベントバインディングをapp.component.htmlに書いて終わり!
<h2>Here are some links to help you start: </h2>
<div align="right">< {{explain}}</div>
<ul>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial" (mouseover)="onButtonIn(0)" (mouseout)="onButtonOut()">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/cli" (mouseover)="onButtonIn(1)" (mouseout)="onButtonOut()">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/" (mouseover)="onButtonIn(2)" (mouseout)="onButtonOut()">Angular blog</a></h2>
</li>
</ul>
リンクの文言にマウスカーソルを乗せるとガイドが回りながら解説を喋り、マウスカーソルを離すとやめるというわけですね。
完成がこちら。
やだ…気持ち悪い。
回っています。「CLI Documentation」にカーソルを乗せているところです。
止まりました。今更ながら、回す必要あったかな?
こんな感じです。解説は手を抜いたので
↓のソースを見て自分で動かしてみてくださいね~。
https://github.com/NodeSleepRest/AngularVtuber
華を添えられた感はゼロなので、3dモデル作成スキルなど持ち合わせていない私はThree.jsから離れて、もう一度このテーマに向き合ってみます。