Blog

GSAP + Three.js & glTF 3D拡大してくるアニメーション(後編)

前編はこちらです。

GSAPタイムラインと組み合わせる

GSAP(グリーン・ソック・アニメーション・プラットフォーム)は、高機能なトゥイーンライブラリです。
かつてはTweenMaxという名称でした。
これを使い出す前は、自前でトリガーのクラスを付与するJS書いてたりしてましたが、もう…戻れません。

※2022年7月にリファクタリングするまでは、HTMLのimg要素は拡大前と拡大後2パターン用意していました。
理由は、アスペクト比を維持したまま位置調整するには、img要素をそのまま使うとやりやすかったこと、拡大後にウインドウサイズを変更した場合に、transformで拡大した画像はうまく画面内で調整されないといったことがありました(canvasにtransformをかけてた)が、パフォーマンスの問題やコードの気持ち悪さが残ったので、後に改善しました。

  • サイズに関しては、ウインドウサイズによるサイズパターンをある程度決めうちにし、JSで分岐する
  • canvasにtransformをかけず、blender側で拡大するアニメーションを作る

として、解決しました。

<div class="instaPhotos-one is-1">
  <div id="insta-1-img" class="instaPhotos-one-img" @click="clickPhoto(1)">
    <img loading="lazy" src="/img/instaphoto-1.jpg" alt="">
  </div>
</div>
〜

GSAPはタイムライン機能があり、それを使って、クリック後に真ん中に飛んで来させます。
飛んでくる時の位置や、幅などを取得しておき、タイムラインを作ります。

openPhoto(){
  // canvasの親要素取得
  const canvasParent = artworkGL.canvas.parentNode;

  openTl = gsap.timeline();

  const window_h = document.documentElement.clientHeight;
  const window_w = document.documentElement.clientWidth;
  // 拡大前の幅と高さ
  let wid,hi;
  let sizeTuning = 0.8;//canvas内のオブジェクトに合わせて掛け率を調整
  if(!mediaSp){
    const maxImgHeight = IMAGE_L_WID / ASPECT;//666
    // ウインドウ高さが高い時の最大値
    if(window_h > maxImgHeight / 0.9){
      hi = maxImgHeight;
    }else{
      hi = window_h * sizeTuning;
    }
    hi = hi / 2;
    wid = hi * ASPECT
  }else{
    wid = window_w / 2 * sizeTuning;
    hi = wid / ASPECT
  }
  // img移動後のtopとleftのラインを求める
  const phototopPos = window_h / 2 - hi / 2;
  const photoleftPos = window_w / 2 - wid / 2;
  // imgSのtopとleftの位置(画面上から)
  const a = this.photoObj.getBoundingClientRect();
  const photoObj_y = a.top;
  const photoObj_x = a.left;

  // Y,X軸どれだけ移動したら良いか
  const imgMoveY = phototopPos - photoObj_y;
  const imgMoveX = photoleftPos - photoObj_x;
  openTl
  .to(this.photoObj, {
    width: wid,
    height: hi,
    x: imgMoveX,
    y: imgMoveY,
    duration: .5,
    ease:'back.out',
  })
  // アニメーション
  .call(this.glAnimation)
  .set(this.photoObj, { opacity: 0})
  .set(canvasParent, { opacity: 1 })

  return openTl;
},

これで、0.5秒でびゅんっとアニメーションします。
そのあとで、artworkGLに用意したアニメーションを呼び出します。

canvasアニメーションスタート

gsapのタイムラインで

.call(this.glAnimation)

を実行します。

callしたglAnimationでは、用意しておいたThree.jsのオブジェクトのアニメーションをスタートさせます。

glAnimation(){
  const current = artworkGL.anime
  
  const animeAction = current.animeAction;
  const mixer = current.mixer;

  current.model.traverse(e => {
    if(e.isMesh){
      e.material.map = artworkGL.texture[this.num];
    }
  })

  animeAction.forEach(e=>{
    //reset前に一度stopしないと、最後のアニメーションが一瞬表示される
    e.stop()
    e.reset()
    e.play()
  })
  artworkGL.clock.start()

  const tick = () => {
    artworkGL.renderer.render(artworkGL.scene, artworkGL.camera);
    //tickキャンセル用に、戻り値を格納しておく
    artworkGL.animeFrame = requestAnimationFrame(tick);
    if (mixer) {
      mixer.update(artworkGL.clock.getDelta());
    }
  }
  tick();
},

model.traverse〜のところで、テクスチャを適用しています。
あらかじめロードして配列に用意しておいたものを呼び出します。

animeActionのplayメソッドを実行すると、アクションをアクティブにするようにミキサーに指示をします。
(これ自体で必ずしもアニメーションスタートにはならない)

renderメソッドでcanvasに一度描かれます。まだ動きません。

renderer.render(シーン,カメラ)

requestAnimationFrameは、引数に渡された関数を毎フレーム実行します。パラパラアニメのようなイメージです。
これでアニメーションがスタートします。
このときにフレームを変数に入れておきます。
canvasアニメーションが終わった後、アニメーションを止めないといけないからです。

グローバルミキサーの時間を進め、アニメーションを更新します。

createClock(){
  artworkGL.clock = new THREE.Clock();
}
〜

mixer.update(artworkGL.clock.getDelta());

canvasの親要素とcanvasのSCSSです。

.artwork{
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  margin: auto;
  max-width: 100%;
  max-height: 100vh;
  transition: transform .1s 0s;
  pointer-events: none;
  z-index: 3;
  backface-visibility: hidden;
  canvas{
    display: block;
    width: 100%;
    height: 100%;
    pointer-events: none;
    .is-photoOpen &{
      pointer-events: inherit;
    }
  }
}

閉じるアニメーション(GSAP) + canvasアニメーションの中止

GSAPのopenのタイムラインを削除します。

openTl.kill();

丸くなって戻るタイムラインを作成。クラス操作でCSSのtransition発動。

const canvasParent = artworkGL.canvas.parentNode;
this.photoObjParent.classList.remove('is-current');
document.body.classList.remove('is-photoOpen');
let closeTl = gsap.timeline();
closeTl
  .set(this.photoObj, { zIndex: 2,scale: 2, opacity: 1 })
  .set(canvasParent, { opacity: 0 })
  .to(this.photoObj, { duration: .7, delay: .3, scale: 1, x: 0, y: 0, width: this.photoObj_w, height: this.photoObj_h, ease: 'back.out' })
  .set(this.photoObj, { zIndex: 0})

CSSは省略しましたが、is-currentクラスでborder-radiusを操作し、is-photoOpenpointer-eventsを操作しています。

canvasアニメーションをキャンセルします。

if(artworkGL.animeFrame){
  window.cancelAnimationFrame(artworkGL.animeFrame);
}

おわりに

リサイズの処理も実際にはありますが、今回は省略しました。

一年前に作ったものをリファクタリングしましたが、一年で知識が増えていたおかげで大分マシなものになった気がします。あとこの頃TypeScript使ってなくて不便に感じました。

これからもいろんな表現ができるようになるべく勉強を続けます。

おすすめの記事 recommend blog

新着 new blog

github