エンジニアの副業は週1からでも可能?副業の例や探し方も解説
- ITエンジニア
- 副業
アニメーションは、ユーザーのWebサイトに対する印象を大きく左右します。裏を返せば、アニメーションによってWebサイトの個性や雰囲気を操作できるということです。
今回はThree.jsを使って、サムネイル画像をフルスクリーンに展開するユニークなアニメーションの作りかたを、チュートリアル形式でご紹介します。ぜひこの記事を参考に、あなたも魅力的なアニメーションを作ってみてください。
目次
作業に取り掛かる前に、まずはエフェクトの基本設定を行いましょう。以下がその手順です。
まずは以下のような、画像が横回転するエフェクトを作成してみましょう。デモの一番のエフェクトです。
まずは基本的な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で問題
次に、画像にあわせて平面のサイズを変更して配置します。
そのためには要素の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イベントにバインドしましょう。すると平面が、要素の画像にあうようにサイズを変えます。
とてもシンプルなコンセプトですが、長期的に考えると非常に効率的です。次のステップでは、この要素の画像をフルスクリーンにしてみましょう。
まずは以下のユニフォームの初期設定を行います。
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.);
}
`;
次に、要素をクリックした際の設定をしましょう。
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();
}
}
要素をクリックするたびにトゥイーンを開始します。どの平面を選んでも、きちんと動作するようになりましたね。
良い感じにできましたね!これで基本的な構成要素が完成しました。ここからさらに、ひとひねり加えていきましょう。
平面全体を拡大縮小するだけではつまらないので、さまざまなパターンを作ってみます。まずは以下の例をご覧ください。
しばらく観察していると、拡大縮小のタイミングを頂点ごとにずらすことで、さまざまな効果が生まれることが分かるでしょう。頂点を動かすタイミングをずらすために、ここでは「アクティベーション」を活用します。
コード上では以下のようになります。
...
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;
...
}
`;
デモで確認してみましょう。(※ちなみにグラデーションは、効果そのものとは特に関係ありません。エフェクトを分かりやすく可視化しただけです)
アクティベーションとタイミングを組み合わせることで、さまざまな動きが可能になります。次はさらに動きを複雑にしてみましょう。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;
...
}
`
これを使うことで、たとえば画像が反転するようなエフェクトにも応用できます。
...
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も反転させることで、この問題を解決できます。ここまでにご紹介したテクニックを組み合わせれば、あらゆる種類のエフェクトが作れます。解説するよりも、実際にアニメーションを見たほうがわかりやすいでしょう。
以下でいくつかご紹介します。
今回は、サムネイル画像をフルスクリーンに展開する際のアニメーションの実装方法をご紹介しました。デモを見ると複雑そうに見えますが、一度構造を理解すれば、さまざまな動きを自由に作れます。
タイミングをずらしたり、反転の方向を変えたりしながら、自分なりのエフェクトを作ってみましょう。
(原文:Daniel Velasquez 翻訳:Nakajima Asuka)
こちらもおすすめ!▼
チュートリアルをなぞるだけ!Three.jsでインタラクティブエフェクトを作ろう
Workship MAGAZINE
Three.jsでパーティクルアニメーションを作ってみよう
Workship MAGAZINE
Three.jsで波形アニメーションを作ってみよう!! ビルがニョキニョキ生えてくる?!
Workship MAGAZINE