Scene切り替え



SceneをまとめたものをChapterと定義する

イベント処理にはPub/Subを採用
[pubsub-js](https://github.com/mroderick/PubSubJS)の使い方
http://qiita.com/mo4_9/items/2ac77bdc7399600bc2d4


## 処理の流れ

1. ChapterManager.js - setChapter | 現在のChapterを決定
2. ChapterManager.js - handleClick | 現在のChapterのhandleClick()を実行
3. Chapter1.js - PubSub.publish | クリックされたオブジェクトに応じてイベントを発行
4. ChapterManager.js - PubSub.subscribe | 購読中のイベントに通知


## Chapter*.js

renderを含まない各Chapterを用意
`Chapter1.js` , `Chapter2.js` , `Chapter3.js`

```javascript:./lib/Chapter1.js
import PubSub from 'pubsub-js';

export default class Chapter1 {
  constructor(opts = {}) {

    this.TOPIC_NAMESPACE = 'Chapter1';

    this.width = opts.width || window.innerWidth;
    this.height = opts.height || window.innerHeight;

    this.init();
  }

  init() {
    { // scene
      this.scene = new THREE.Scene();
    }
    { // camera
      this.camera = new THREE.PerspectiveCamera( 45, this.width / this.height, 1, 10000 ); // fov(視野角),aspect,near,far
      this.camera.position.set(100, 100, 100);
      this.camera.up.set(0, 1, 0);
      this.camera.lookAt({ x:0, y:0, z:0 });
    }
    { // controls
      this.controls = new THREE.OrbitControls(this.camera);
    }
    { // lights
      const ambientLight = new THREE.AmbientLight(0xffffff);
      this.scene.add(ambientLight);
    }
    { // axis
      const axis = new THREE.AxisHelper(1000);
      axis.position.set(0,0,0);
      this.scene.add(axis);
    }
    { // grid
      const grid = new THREE.GridHelper(50, 10); // size, step
      this.scene.add(grid);
    }
    { // cube
      const cgeometry = new THREE.CubeGeometry(20, 20, 20);
      const cmaterial = new THREE.MeshLambertMaterial({ color: 0xffcc66 });
      this.cube = new THREE.Mesh(cgeometry, cmaterial);
      this.cube.name = `cube-${this.TOPIC_NAMESPACE}`;
      this.scene.add(this.cube);
    }
    { // sphere
      const sgeometry = new THREE.SphereGeometry(10, 30, 30);
      const smaterial = new THREE.MeshLambertMaterial({ color: 0x66ccff });
      this.sphere = new THREE.Mesh(sgeometry, smaterial);
      this.sphere.name = `sphere-${this.TOPIC_NAMESPACE}`;
      this.sphere.position.set(10,50,-10);
      this.scene.add(this.sphere);
    }
  }

  handleClick(objs) {
    // クリックされたオブジェクトに応じて、発行するイベントを分岐する
    const isCubeClick = objs.some(obj => obj.object.name == 'cube-Chapter1'); // nameで判定
    const isSphereClick = objs.some(obj => obj.object == this.sphere); // objectで判定
    console.log(objs, isCubeClick, isSphereClick);
    if (isCubeClick) {
      PubSub.publish(`nextObjClick.${this.TOPIC_NAMESPACE}`);
    }
    if (isSphereClick) {
      PubSub.publish(`prevObjClick.${this.TOPIC_NAMESPACE}`);
    }
  }
}
```

## ChapterManager.js

```javascript:./lib/ChapterManager.js
import PubSub from 'pubsub-js';
import './OrbitControls';
import Chapter1 from './Chapter1';
import Chapter2 from './Chapter2';
import Chapter3 from './Chapter3';

export default class ChapterManager{
  constructor(opts = {}) {
    this.RENDER_INTERVAL = 30;
    this.TICK_INTERVAL = 1500;

    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.container = opts.container || document.createElement('div');

    this.chapterList = [
      new Chapter1({
        width: this.width,
        height: this.height,
      }),
      new Chapter2({
        width: this.width,
        height: this.height,
      }),
      new Chapter3({
        width: this.width,
        height: this.height,
      }),
    ];

    this.init();
    this.setEvent();
    this.setChapter();
  }

  init() {
    { // renderer
      this.renderer = new THREE.WebGLRenderer({ antialias: true });
      this.renderer.setClearColor( 0x222222 ); // 背景色
      this.renderer.setPixelRatio(window.devicePixelRatio || 1);
      this.renderer.setSize( this.width, this.height );
      this.container.appendChild( this.renderer.domElement );
    }
  }

  setEvent() {
    const rect = this.renderer.domElement.getBoundingClientRect();

    this.renderer.domElement.addEventListener('mousedown', (evt) => {
      const mouseX = evt.clientX - rect.left;
      const mouseY = evt.clientY - rect.top;
      const position = {
        x: (mouseX / rect.width) * 2 - 1, // -1 ~ +1 の座標
        y: -(mouseY / rect.height) * 2 + 1, // -1 ~ +1 の座標
      };
      const objs = this.getIntersectObjects(position.x, position.y);

      if (objs.length > 0) {
        if (!this.currentChapter.handleClick) {
          return;
        }
        this.currentChapter.handleClick(objs);
      }
    }, false);

    PubSub.subscribe('nextObjClick.Chapter1', () => {
      this.setChapter(1);
    });
    PubSub.subscribe('prevObjClick.Chapter1', () => {
      this.setChapter(2);
    });

    PubSub.subscribe('nextObjClick.Chapter2', () => {
      this.setChapter(2);
    });
    PubSub.subscribe('prevObjClick.Chapter2', () => {
      this.setChapter(0);
    });

    PubSub.subscribe('nextObjClick.Chapter3', () => {
      this.setChapter(0);
    });
    PubSub.subscribe('prevObjClick.Chapter3', () => {
      this.setChapter(1);
    });
  }

  // sceneとcameraを現在のchapterに更新する
  setChapter(id = 0) {
    this.currentChapter = this.chapterList[id];
    console.log(this.currentChapter);
    this.scene = this.currentChapter.scene;
    console.log(this.scene);
    this.camera = this.currentChapter.camera;
  }

  getIntersectObjects(x, y) {
    const vector = new THREE.Vector3(x, y, 1);
    vector.unproject(this.camera);

    const ray = new THREE.Raycaster(this.camera.position, vector.sub(this.camera.position).normalize());
    const objs = ray.intersectObjects(this.scene.children);

    return objs;
  }

  start() {
    const startTime = Date.now();
    let previousTime = startTime;
    let previousRenderTime = previousTime;
    let previousTickTime = previousTime;
    this.loopId;

    const loop = (timestamp) => {
      const nowTime = Date.now();
      const elapsedTime = nowTime - startTime;
      const deltaTime = nowTime - previousTime;
      const deltaRenderTime = nowTime - previousRenderTime;
      const deltaTickTime = nowTime - previousTickTime;

      if (deltaRenderTime > this.RENDER_INTERVAL) {
        previousRenderTime = nowTime;
        this.render();
      }
      if (deltaTickTime > this.TICK_INTERVAL) {
        previousTickTime = nowTime;
        this.tick();
      }

      previousTime = nowTime;
      this.loopId = requestAnimationFrame(loop);
    };

    loop();
  }

  stop() {
    cancelAnimationFrame(this.loopId);
  }

  tick() {
  }

  render() {
    this.renderer.render( this.scene, this.camera );
  }
}
```

```javascript:script.js
import ChapterManager from './lib/ChapterManager';

(() => {
  const chapterManager = new ChapterManager({
    container: document.getElementById('canvas-container'),
  });

  chapterManager.start();
})();
```