LoginSignup
32
23

More than 5 years have passed since last update.

three.js超入門 第4回 getBoundingClientRect()を使ったDOM要素との連携

Last updated at Posted at 2018-12-17

概要

この記事では「three.js超入門」と題して、three.jsの基礎からシェーダーの利用までをやっていきます。
ターゲットは主に「canvas表現を触ったことがないフロントエンドエンジニア」を想定しているので、jsの構文などの説明は省略しています。
three.jsのバージョンは執筆時点で最新のr98を使用します。

three.js超入門 第0回 3Dコンピュータグラフィックスの基礎
three.js超入門 第1回 レンダリングまでの流れ
three.js超入門 第2回 アニメーションと時間ベースでの制御
three.js超入門 第3回 マウスやスクロールでのインタラクション
three.js超入門 第4回 DOM要素との連携
three.js超入門 第5回 シェーダー(GLSL)の基礎
three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する
three.js超入門 第7回 シェーダーに変数を渡す
three.js超入門 第8回 シェーダーをインタラクティブに動かす
three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける

リポジトリ

前回はウィンドウのマウスやスクロールなどのイベントから取れる値を使って3Dオブジェクトをインタラクティブに動かしました。
今回はDOM要素の位置と大きさをthree.jsのオブジェクトに反映させて、3DオブジェクトをDOM要素の背景として利用してみます。

下準備

常に回転していると邪魔なので、まず回転に関する記述をコメントアウトしておきます。

export default class Canvas {
  constructor() {
    // ~ 省略 ~

    // マテリアルを作成
    const mat = new MeshLambertMaterial({ color: 0xffffff });

    // ジオメトリとマテリアルからメッシュを作成
    this.mesh = new Mesh(geo, mat);
    // this.mesh.rotation.x = Math.PI / 4;
    // this.mesh.rotation.y = Math.PI / 4;

    // ~ 省略 ~
  }

  render() {
    // 次のフレームを要求
    requestAnimationFrame(() => { this.render(); });

    // ミリ秒から秒に変換
    // const sec = performance.now() / 1000;

    // 1秒で45°回転する
    // this.mesh.rotation.x = sec * (Math.PI / 4);
    // this.mesh.rotation.y = sec * (Math.PI / 4);

    // スクロールに追従させる
    // this.mesh.position.y = this.scrollY * 0.5;

    // 画面に表示
    this.renderer.render(this.scene, this.camera);
  }

  // ~ 省略 ~
};

DOM要素のウィンドウ上の矩形を取得

Canvasクラス内でDOM要素を取得するために、コンストラクタでidを引数で取ってくるように変更します。

00_empty/index.js
import Canvas from './Canvas';

export default class Page00 {
  constructor() {
    const canvas = new Canvas('scroll-container_title');

    // ~ 省略 ~
  }
};

getElementById()で対象のDOM要素を取得したら、element.getBoundingClientRect()でその要素の現在のウィンドウ上の矩形(DOMRect)を取得します。

element.getBoundingClientRect()で取得できる値は、ウィンドウの絶対座標なので、スクロールをするたびに値が更新されます。(cssでいうところのposition: fixed;にしたときの座標をイメージするとわかりやすいかとおもいます。)

00_empty/Canvas/index.js
export default class Canvas {
  constructor(elementId) {
    // elementIdのついたDOM要素を取得
    this.element = document.getElementById(elementId);
    const rect   = this.element.getBoundingClientRect();

    // ~ 省略 ~
  }
};

rectをコンソールにログ出しすると以下のようになっています。
スクリーンショット 2018-12-17 16.07.17.png
x, ytop, leftには同じ値が入っていますが、x, yは使用できるブラウザが限られているので、top, leftを使います。

DOMRectのサイズをジオメトリに反映

DOMRectの幅と高さをBoxGeometryに反映させます。
this.mesh.position.z = -depth / 2とすることで、直方体の半分前に出ている奥行きを補正して、z = 0のところに面がくるように補正しています。

// ~ 省略 ~

export default class Canvas {
  constructor(elementId) {
    // elementIdのついたDOM要素を取得
    this.element = document.getElementById(elementId);
    const rect   = this.element.getBoundingClientRect();

    // ~ 省略 ~

    // DOMRectサイズの直方体ジオメトリを作成(幅, 高さ, 奥行き)
    const depth = 300;
    const geo   = new BoxGeometry(rect.width, rect.height, depth);

    // ~ 省略 ~

    // ジオメトリとマテリアルからメッシュを作成
    this.mesh = new Mesh(geo, mat);
    this.mesh.position.z = -depth / 2;// 奥行きの半分前に出ているのを下げる

    // ~ 省略 ~
  }

  // ~ 省略 ~
};

最初の位置はまだずれていますが、スクロールして位置を合わせて検証ツールで見るとDOMとメッシュのサイズが一致していることがわかります。
スクリーンショット 2018-12-17 16.23.41.png

DOMRectの位置をメッシュに反映

メッシュの位置は中心指定で、DOMRectの位置は左上指定なので、DOMRectの中心座標を求めてウィンドウの中心との差分をオフセットさせることで位置合わせができます。

// ~ 省略 ~

export default class Canvas {
  constructor(elementId) {
    // elementIdのついたDOM要素を取得
    this.element = document.getElementById(elementId);
    const rect   = this.element.getBoundingClientRect();

    // ~ 省略 ~

    // ジオメトリとマテリアルからメッシュを作成
    this.mesh = new Mesh(geo, mat);
    // this.mesh.position.z = -depth / 2;// 奥行きの半分前に出ているのを下げる(↓に統合)

    // ウィンドウ中心からDOMRect中心へのベクトルを求めてオフセットする
    const center = new Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2);
    const diff   = new Vector2(center.x - this.w / 2, center.y - this.h / 2);
    this.mesh.position.set(diff.x, -diff.y, -depth / 2);

    // ~ 省略 ~
  }

  // ~ 省略 ~
};

これでサイズと位置がぴったり合いました。
スクリーンショット 2018-12-17 16.41.37.png

DOMRectの位置、サイズを維持しつつスクロール追従させる

メッシュの位置を設定する部分にデフォルトのスクロール量(this.scrollY)を足して、オフセット値を変数に保存します。

this.mesh.position.set(diff.x, -(diff.y + this.scrollY), -depth / 2);
this.offsetY = this.mesh.position.y;

render関数内でコメントアウトしていたスクロール追従の部分のコメントを外して、this.offsetYを足します。

this.mesh.position.y = this.offsetY + this.scrollY;

Kapture 2018-12-17 at 16.53.26.gif

これでDOM要素にぴったりついてくる3Dオブジェクトができました!
ライティングによる表現はcssでは実現が難しいので、ボックスではなく平面を置くだけでも普通のWebサイトとは違う、一風変わった表現ができるとおもいます。

32
23
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
23