Three.jsで作る、サムネイルをフルスクリーンに展開するアニメーション

Webエフェクト

Webエフェクト

アニメーションは、ユーザーのWebサイトに対する印象を大きく左右します。裏を返せば、アニメーションによってWebサイトの個性や雰囲気を操作できるということです。

今回はThree.jsを使って、サムネイル画像をフルスクリーンに展開するユニークなアニメーションの作りかたを、チュートリアル形式でご紹介します。ぜひこの記事を参考に、あなたも魅力的なアニメーションを作ってみてください。

はじめに

作業に取り掛かる前に、まずはエフェクトの基本設定を行いましょう。以下がその手順です。

  • Three.jsと平面の初期設定を行う
  • ユーザーが要素の画像をクリックしたときと同じように平面を配置し、拡大縮小する
  • 平面にアニメーションをつけ、画面全体を覆うようにする

まずは以下のような、画像が横回転するエフェクトを作成してみましょう。デモの一番のエフェクトです。

GridToFullscreen_2

ステップ1. 初期設定

まずは基本的なThree.jsの設定をしましょう。一度にひとつのアニメーションしか実行できないため、まずは1×1の平面を追加し、すべてのグリッドアイテムのアニメーションに適用します。このシンプルな工夫によって、アニメーションのパフォーマンスに影響を与えることなく、好きなだけHTML要素を使用できるようになります。

ちなみに、今回はアニメーションにのみThree.jsを使用し、それ以外は従来のHTMLでまかないます。これにより、WebGLをサポートしていないブラウザへの対応も可能になるのです。

class GridToFullscreenEffect {
	...
	init(){
		... 
		const segments = 128;
		var geometry = new THREE.PlaneBufferGeometry(1, 1, segments, segments);
		// We'll be using the shader material later on ;)
		var material = new THREE.ShaderMaterial({
		  side: THREE.DoubleSide
		});
		this.mesh = new THREE.Mesh(geometry, material);
		this.scene.add(this.mesh);
	}
}

Three.jsの初期化についての説明は、基礎知識で対応できると思うのでここでは省きます。

作業をシンプルにするために、平面のジオメトリのサイズは1×1に設定しましょう。任意の数値にスケーリングされるため、1×1で問題

ステップ2. ポジショニングとリサイズ

次に、画像にあわせて平面のサイズを変更して配置します。

そのためには要素のgetBoundingClientRectを取得し、その値をピクセルからカメラの視野単位に変換しなければいけません。さらにその後、左上からの相対位置を、中央からの相対位置に変換します。

以下が手順のまとめです。

  • ピクセル単位をカメラの視野単位にマッピングする
  • 単位の基準を左上ではなく中央にする
  • 位置の原点は左上ではなく平面の中心にする
  • これらの新しい値を使用し、メッシュを拡大縮小して配置する
class GridToFullscreenEffect {
...
 onGridImageClick(ev,itemIndex){
	// getBoundingClientRect gives pixel units relative to the top left of the pge
	 const rect = ev.target.getBoundingClientRect();
	const viewSize = this.getViewSize();
	
	// 1. Transform pixel units to camera's view units
	const widthViewUnit = (rect.width * viewSize.width) / window.innerWidth;
	const heightViewUnit = (rect.height * viewSize.height) / window.innerHeight;
	let xViewUnit =
	  (rect.left * viewSize.width) / window.innerWidth;
	let yViewUnit =
	  (rect.top * viewSize.height) / window.innerHeight;
	
	// 2. Make units relative to center instead of the top left.
	xViewUnit = xViewUnit - viewSize.width / 2;
	yViewUnit = yViewUnit - viewSize.height / 2;
   

	// 3. Make the origin of the plane's position to be the center instead of top Left.
	let x = xViewUnit + widthViewUnit / 2;
	let y = -yViewUnit - heightViewUnit / 2;

	// 4. Scale and position mesh
	const mesh = this.mesh;
	// Since the geometry's size is 1. The scale is equivalent to the size.
	mesh.scale.x = widthViewUnit;
	mesh.scale.y = heightViewUnit;
	mesh.position.x = x;
	mesh.position.y = y;

	}
 }

ちなみにジオメトリではなく、メッシュを拡大縮小するほうがパフォーマンスが向上します。ジオメトリを拡大縮小すると、レンダリング時にメッシュの拡大縮小がおこなわれますが、内部データの変更が遅くなってしまうのです。

それでは、この機能を各要素のonclickイベントにバインドしましょう。すると平面が、要素の画像にあうようにサイズを変えます。

 とてもシンプルなコンセプトですが、長期的に考えると非常に効率的です。

ステップ3. ベーシックなアニメーション

次のステップでは、この要素の画像をフルスクリーンにしてみましょう。

まずは以下のユニフォームの初期設定を行います。

  • uProgress:アニメーションの進行
  • uMeshScale:メッシュのスケール
  • uMeshPosition:中心からのメッシュの位置
  • uViewSize:カメラ視野のサイズ
class GridToFullscreenEffect {
	constructor(container, items){
		this.uniforms = {
		  uProgress: new THREE.Uniform(0),
		  uMeshScale: new THREE.Uniform(new THREE.Vector2(1, 1)),
		  uMeshPosition: new THREE.Uniform(new THREE.Vector2(0, 0)),
		  uViewSize: new THREE.Uniform(new THREE.Vector2(1, 1)),
		}
	}
	init(){
		... 
		const viewSize = this.getViewSize();
		this.uniforms.uViewSize.x = viewSize.width;
		this.uniforms.uViewSize.y = viewSize.height;
		var material = new THREE.ShaderMaterial({
			uniform: this.uniforms,
			vertexShader: vertexShader,
			fragmentShader: fragmentShader,
			side: THREE.DoubleSide
		});
		
		...
	}
	...
}
const vertexShader = `
	uniform float uProgress;
	uniform vec2 uMeshScale;
	uniform vec2 uMeshPosition;
	uniform vec2 uViewSize;

	void main(){
		vec3 pos = position.xyz;
		 gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.);
	}
`;
const fragmentShader = `
	void main(){
		 gl_FragColor = vec4(vec3(0.2),1.);
	}
`;

要素をクリックするたびに、uMeshScaleとuMeshPositonのユニフォームを更新する必要があります。

class GridToFullscreenEffect {
	...
	onGridImageClick(ev,itemIndex){
		...
		// Divide by scale because on the fragment shader we need values before the scale 
		this.uniforms.uMeshPosition.value.x = x / widthViewUnit;
		this.uniforms.uMeshPosition.value.y = y / heightViewUnit;

		this.uniforms.uMeshScale.value.x = widthViewUnit;
		this.uniforms.uMeshScale.value.y = heightViewUnit;
	}
}

ジオメトリではなくメッシュを拡大縮小したので、頂点シェーダーにおいて、頂点は依然として1×1の正方形をシーンの中心に表示しているはずです。しかし最終的には、メッシュのために異なるサイズで別の位置にレンダリングされます。そのために頂点シェーダーでは「縮小」された値を使用しなければいけません。

頂点シェーダーでエフェクトを実行してみましょう。手順は以下のとおりです。

  • メッシュのスケールを使用して、画面サイズにあわせた必要なスケールを計算する
  • 頂点を中心に移動させる
  • これらの値にエフェクトをかける
...
const vertexShader = `
	uniform float uProgress;
	uniform vec2 uPlaneSize;
	uniform vec2 uPlanePosition;
	uniform vec2 uViewSize;

	void main(){
		vec3 pos = position.xyz;
		
		// Scale to page view size/page size
		vec2 scaleToViewSize = uViewSize / uPlaneSize - 1.;
		vec2 scale = vec2(
		  1. + scaleToViewSize * uProgress
		);
		pos.xy *= scale;
		
		// Move towards center 
		pos.y += -uPlanePosition.y * uProgress;
		pos.x += -uPlanePosition.x * uProgress;
		
		
		 gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.);
	}
`;

次に、要素をクリックした際の設定をしましょう。

  • キャンバスコンテナを要素の上におく
  • HTML要素を非表示にする
  • 0から1のあいだのuProgressにてトゥイーン(2つの画像が変化しながら繋がること)を実行する
class GridToFullscreenEffect {
	...
	constructor(container,items){
		...
		this.itemIndex = -1;
		this.animating = false;
		this.state = "grid";
	}
	toGrid(){
		if (this.state === 'grid' || this.isAnimating) return;
		this.animating = true;
		this.tween = TweenLite.to(
		  this.uniforms.uProgress,1.,
		  {
			value: 0,
			onUpdate: this.render.bind(this),
			onComplete: () => {
			  this.isAnimating = false;
			  this.state = "grid";
			this.container.style.zIndex = "0";
			}
		  }
		);
	}
	toFullscreen(){
	if (this.state === 'fullscreen' || this.isAnimating) return;
		this.animating = true;
		this.container.style.zIndex = "2";
		this.tween = TweenLite.to(
		  this.uniforms.uProgress,1.,
		  {
			value: 1,
			onUpdate: this.render.bind(this),
			onComplete: () => {
			  this.isAnimating = false;
			  this.state = "fullscreen";
			}
		  }
		);
	}

	onGridImageClick(ev,itemIndex){
		...
		this.itemIndex = itemIndex;
		this.toFullscreen();
	}
}

要素をクリックするたびにトゥイーンを開始します。どの平面を選んでも、きちんと動作するようになりましたね。

良い感じにできましたね!

これで基本的な構成要素が完成しました。ここからさらに、ひとひねり加えていきましょう。

ステップ4. アクティベーションとタイミング

平面全体を拡大縮小するだけではつまらないので、さまざまなパターンを作ってみます。まずは以下の例をご覧ください。

GridToFullscreen_3

しばらく観察していると、拡大縮小のタイミングを頂点ごとにずらすことで、さまざまな効果が生まれることが分かるでしょう。頂点を動かすタイミングをずらすために、ここでは「アクティベーション」を活用します。

GridToFullscreen_4

コード上では以下のようになります。

...
const vertexShader = `
	...
	void main(){
		vec3 pos = position.xyz;
		
		// Activation for left-to-right
		float activation = uv.x;
		
		float latestStart = 0.5;
		float startAt = activation * latestStart;
		float vertexProgress = smoothstep(startAt,1.,uProgress);
	   
		...
	}
`;

頂点シェーダーにおけるすべての計算で、uProgressをvertexprogresに置き換えます。

...
const vertexShader = `
	...
	void main(){
		...
		float vertexProgress = smoothstep(startAt,1.,uProgress);
		
		vec2 scaleToViewSize = uViewSize / uMeshScale - 1.;
		vec2 scale = vec2(
		  1. + scaleToViewSize * vertexProgress
		);
		pos.xy *= scale;
		
		// Move towards center 
		pos.y += -uMeshPosition.y * vertexProgress;
		pos.x += -uMeshPosition.x * vertexProgress;
		...
	}
`;

デモで確認してみましょう。(※ちなみにグラデーションは、効果そのものとは特に関係ありません。エフェクトを分かりやすく可視化しただけです)

アクティベーションとタイミングを組み合わせることで、さまざまな動きが可能になります。

ステップ5. トランスフォーメーション

次はさらに動きを複雑にしてみましょう。vertexProgressを使って、ある状態から別の状態に移動(または補完)させます。

...
const vertexShader = `
	...
	void main(){
	...
		// Base state = 1.
		// Target state = uScaleToViewSize;
		// Interpolation value: vertexProgress
		scale = vec2(
		  1. + uScaleToViewSize * vertexProgress
		);

		// Base state = pos
		// Target state = -uPlaneCenter;
		// Interpolation value: vertexProgress
		pos.y += -uPlaneCenter.y * vertexProgress;
		pos.x += -uPlaneCenter.x * vertexProgress;
	...
	}
`

これを使うことで、たとえば画像が反転するようなエフェクトにも応用できます。

  • Base state:頂点の現在位置
  • Target state:頂点の反転位置
  • Interpolation with:頂点の進捗
...
const vertexShader = `
	...
	void main(){
		...
		float vertexProgress = smoothstep(startAt,1.,uProgress);
		// Base state: pos.x
		// Target state: flippedX
		// Interpolation with: vertexProgress 
		float flippedX = -pos.x;
		pos.x = mix(pos.x,flippedX, vertexProgress);
		// Put vertices that are closer to its target in front. 
		pos.z += vertexProgress;
		...
	}
`;

反転で頂点が重なる際には、違和感のないように頂点の位置をずらして調整しましょう。こうした反転エフェクトをアクティベーションと組み合わせると、以下のようなアニメーションが作れます。

アニメーションをよく観察すると、色や画像も反転してしまっていることがわかります。UVも反転させることで、この問題を解決できます。

完成!

ここまでにご紹介したテクニックを組み合わせれば、あらゆる種類のエフェクトが作れます。解説するよりも、実際にアニメーションを見たほうがわかりやすいでしょう。

以下でいくつかご紹介します。

タイミングをずらしたアニメーション

GridToFullscreen_5

ゆがみながら拡大縮小するアニメーション

GridToFullscreen_6

遠近感を感じさせるアニメーション

GridToFullscreen_7

おわりに

今回は、サムネイル画像をフルスクリーンに展開する際のアニメーションの実装方法をご紹介しました。デモを見ると複雑そうに見えますが、一度構造を理解すれば、さまざまな動きを自由に作れます。

タイミングをずらしたり、反転の方向を変えたりしながら、自分なりのエフェクトを作ってみましょう。

(原文:Daniel Velasquez 翻訳:Nakajima Asuka)

 

こちらもおすすめ!▼

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
デザイナー副業案件
Workship