microCMS × Babyon.jsでフォトギャラリーサイトを作成

2024/08/10に公開

背景

今までmicroCMS・Next.js・Astro・Babyon.jsなど色々インプットして、ポートフォリオサイトを作成したりBabyon.jsで3D表現してcodepenに掲載しておりました。
今回は新たな挑戦としてmicoCMS×Babyon.jsを組み合わせて何か面白いものを作れないかと色々と考えた結果、3D形状でフォトギャラリーサイトを作ろうと考えました。

今までその実例はないので試行錯誤して実装しました。

作ったもの

作ったものが以下になります。
microCMSで投稿したものを3D状でカルーセル表示してクリックすると画像が拡大できるようになってます。
他にもライトモードダークモード切り替えスイッチやカルーセル・背景マウスムーブ切り替えスイッチなどもつけております。

https://n-sports.vercel.app


microCMSで投稿したものを3D形状でカルーセル表示


拡大表示


カルーセル・背景マウスムーブ切り替えスイッチ


ライトモード・ダークモード切り替えスイッチ

言語・フレームワーク

利用した言語・フレームワークは以下です

・フレームワーク:Astro
・CSS:Tailwind CSS
・JS:Vanilla JavaScript
・CMS:microCMS
・3D エンジン:Babylon.js
・ホスティング:Vercel

Astro・Vercelなどモダンなホスティング・フレームワークを使っていますが、今回はBabylon.js・microCMSについてお話ししていきます。 Astro・Tailwind CSS・Vercelに関しては割愛します。

Babylon.js・microCMSについて

Babylon.jsとは

自分が投稿した以下の記事にも掲載しておりますが、Babylon.jsはMicrosoft社が開発したJavaScriptのWebGLのライブラリです。
有名なThreeJSと同じWebGLになります。
Babylon.jsはWeb上3Dゲーム開発やWebXRなど幅広く利用されているようです。

https://www.babylonjs.com/

https://zenn.dev/uemuragame5683/articles/b21bd089f444de#babylon.jsとは

microCMSとは

microCMSは多くのメンバーがご利用している日本発のヘッドレスCMS(Content Management System、コンテンツ管理システム)です。microCMSはAPIベースで動作し、ウェブサイトやアプリケーションに動的コンテンツを提供しています。

https://microcms.io/

Babylon.js・microCMSを説明した所でこれらを組み合わせて実装していきます。

実装する

microCMSを登録して記事投稿する

まずは、microCMSを登録します。
登録後、今回のAPIは以下のように設定します。

API名 - ゴルフ
エンドポイント - golf
APIの型 - リスト形式

Astro・TailwindCSSで環境準備

フレームワークはAstroを利用して環境構築します。
以下のようにコマンドを打ってAstroをインストールします。

npm create astro@latest

すると以下のような感じにディレクトリが生成されます。

public/
    favicon.svg
src/
    components/
        Header.astro
        Button.jsx
layouts/
    Layout.astro
pages/
    index.astro
styles/
    global.css
astro.config.mjs
package.json
tsconfig.json

AstroインストールしたらTailWindCSSをインストールします。

npm install -D tailwindcss

インストール後「tailwind.config.js」を生成して以下のように設定します。

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  mode: "jit",
  prefix: "tw-",
  content: [
    "src/components/*.astro",
    "src/layouts/*.astro",
    "src/pages/*.astro",
  ],
  theme: {
    extend: {
      screens: {
        'sm': {'min': '768px'},
        'md': {'min': '980px'},
        'sm-max': {'max': '767px'},
        'md-max': {'max': '979px'}
      },   
    },
  },
  plugins: [],
}

microCMS <> Astroを紐付ける

microCMS SDKをインストールする

次に、microCMS SDKを利用してmicroCMSで投稿したものを呼び出します。
まずは以下のようにmicrocms-js-sdkをインストールします。

npm install microcms-js-sdk

SDKをインポートし設定する

インストール後、.envファイルを開き環境設定して、
「library/microcms.ts」ファイルを作成して以下のように設定します。

.env
MICROCMS_API_KEY=xxxxxxxxxxxxxxxxx
MICROCMS_SERVICE_DOMAIN=xxxxx
library/microcms.ts
// SDK利用準備
import { createClient, MicroCMSQueries } from "microcms-js-sdk";

const nu_client = createClient({
  serviceDomain: import.meta.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: import.meta.env.MICROCMS_API_KEY,
});

// 型定義
export type Blog = {
  id: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  revisedAt: string;
  title: string;
  content: string;
};

export type BlogResponse = {
  totalCount: number;
  offset: number;
  limit: number;
  contents: Blog[];
};

// n-sportsの呼び出し
export const getNsports = async (queries?: MicroCMSQueries) => {
  return await nu_client.get<BlogResponse>({ endpoint: "golf", queries: { limit: 99 } });
};

フロント実装する

Layout.astroを変更する

次にでフロント実装していきます。
Layout.astroのファイルを開き以下のように構築していきます。(一部省略)

src/layouts/Layout.astro
---
import Svg from '../components/Svg.astro';
---

<!doctype html>
<html lang="jp">
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content="Astro description" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <link href="/tailwind.css" rel="stylesheet" />
    <script is:inline src="https://preview.babylonjs.com/babylon.js"></script>
    <script is:inline src="https://cdn.babylonjs.com/materialsLibrary/babylon.gridMaterial.js"></script>
    <script is:inline src="https://code.jquery.com/pep/0.4.1/pep.js"></script>
    <script is:inline src="https://unpkg.com/earcut@latest/dist/earcut.min.js"></script>
    <script is:inline src="https://cdn.babylonjs.com/materialsLibrary/babylon.waterMaterial.js"></script>
    <title>N sports</title>
  </head>
  <body>
    <header class="tw-relative tw-z-[10] tw-px-[20px] tw-py-[10px] tw-btsm-max:tw-p-[10px]">
      <h1 class="tw-flex tw-flex-wrap tw-gap-[10px] tw-text-[36px] tw-text-shadow-white sm-max:tw-text-[24px]">
        N sports
      </h1>	
      <div class="tw-absolute tw-z-[10] tw-px-[20px] tw-py-[25px] tw-top-[0px] tw-right-[0px] tw-gap-[10px] tw-flex">
        <div class="tw-gap-[10px] tw-flex tw-mr-[10px]">
          <button type="button" class="js-caroucel-button tw-filter-drow-shadow" role="button" id="caoucel_switch">
            <Svg name="js-caroucelmove"/>
          </button>
          <button type="button" class="js-caroucel-button tw-opacity-50" role="button" id="background_switch">
            <Svg name="js-pcmove"/>
          </button>
        </div>
        <div class="tw-gap-[10px] tw-flex">
          <button type="button" class="js-mode-button tw-filter-drow-shadow" role="button" id="light">
            <Svg name="js-lightmode"/>
          </button>
          <button type="button" class="js-mode-button tw-opacity-50" role="button" id="dark">
            <Svg name="js-darkmode"/>
          </button>
        </div>    
      </div>
    </header>
    <slot />
    <footer class="tw-absolute tw-bottom-[0px] tw-z-[10] tw-p-[20px] tw-block tw-w-[100%] tw-text-shadow-white sm-max:tw-p-[10px]">
      (c) {new Date().getFullYear()} N sports
    </footer>
  </body>
</html>
・・・以降省略

トップページの実装

トップページの全体のレイアウトは以下のように組み込みます。

src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
import Animation from '../components/Animation.astro';
---

<Layout>
  <main class="tw-absolute tw-top-[0px] tw-left-[0px] tw-w-[100%] tw-h-[100vh]">
    <div id="js-loading" class="tw-fixed tw-top-[0px] tw-left-[0px] tw-bg-[#000000] tw-w-[100%] tw-h-[100vh] tw-z-[1000]">
      <div class="tw-animate-loading-animation tw-bg-gradient-to-r tw-from-white tw-from-30% tw-to-black tw-w-[80px] tw-h-[80px] tw-p-[3px] tw-flex tw-justify-center tw-items-center tw-rounded-[50%] tw-top-[calc(50%_-_40px)] tw-left-[calc(50%_-_40px)] tw-absolute">
        <div class="tw-w-[100%] tw-h-[100%] tw-bg-[#000000] tw-rounded-[50%]"></div>
      </div>
    </div>
    <Animation />	
    <Card />
  </main>
</Layout>

後背景・ライトモード・ダークモードの実装

次に、後背景(ゴルフ場) を表示  + ライトモード・ダークモードを実装します。
class関数を利用して実装します。(一部省略)

src/components/Animation.astro
<canvas id="renderCanvas" class="tw-rendercanvas"></canvas>

<script type="text/javascript">
  class LightDarkmode {
    constructor() {
      this.degree();
      this.mode_switch = document.getElementsByClassName('js-mode-button');
      this.clickevent( this.mode_switch );
      this.light_dark();
      this.renderfunction();
      this.createScene();
      this.pointerLight();
      this.sky_ground_water_setting();
      this.tea_setting();
      this.cup_setting();
      this.tree_setting();
      this.lump_setting();
      this.fountain_setting();
    }
    degree(degrees) {
      // 度数を取得
      switch(true) {
        case degrees !== undefined:
          return degrees * (Math.PI / 180);
          break;
        default:
          return null;
          break;
      }
    }
    clickevent(target) {
      // ライトモード・ダークモードをクリックした時の処理
      for(let i = 0; i < target.length; i++){
        target[i].addEventListener("click",(e) => {
          this.light_dark(e.target.getAttribute('id'));
        }, false);
      }
    }
    light_dark(target) {
      // ライトモード・ダークモードの処理
      switch(true) {
        case target === 'dark':
          document.getElementById('light').classList.add('tw-opacity-50');
          document.getElementById('light').classList.remove('tw-filter-drow-shadow');
          document.getElementById('dark').classList.add('tw-filter-drow-shadow');
          document.getElementById('dark').classList.remove('tw-opacity-50');
          break;
        default:
          document.getElementById('light').classList.add('tw-filter-drow-shadow');        
          document.getElementById('light').classList.remove('tw-opacity-50');
          document.getElementById('dark').classList.add('tw-opacity-50');
          document.getElementById('dark').classList.remove('tw-filter-drow-shadow');
          break;          
      }
      const canvas = document.getElementById('renderCanvas');
            canvas.remove();
      const canvas_element = document.createElement('canvas');
            canvas_element.setAttribute("id",'renderCanvas');
      const main = document.getElementsByTagName("main");
            main[0].prepend(canvas_element);
      const new_canvas = document.getElementById('renderCanvas');
            new_canvas.classList.add('tw-rendercanvas');      
      const engine = new BABYLON.Engine(new_canvas);

      this.renderfunction(target);
    }
    renderfunction(light_dark) {
        // ビジュアルをレンダリングする
        const loading = document.getElementById("js-loading");
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas);      
        const scene = this.createScene( this.degree, light_dark, engine, canvas );
        
        loading.classList.remove('tw-hidden');

        engine.runRenderLoop(() => {
          scene.render();
        });
        
        window.addEventListener('resize', () => {
          engine.resize();
        });

        setTimeout(() => {
          loading.classList.add('tw-hidden');
        }, 1000);
    }
    pointerLight(light_dark, scene) {
      // ライト設定
      switch(true) {
        case !( light_dark == undefined && scene == undefined ):
          switch( true ) {
            case light_dark == 'dark':
              const spot = new BABYLON.PointLight("spot", new BABYLON.Vector3(5, 5, 5));
                    spot.diffuse = new BABYLON.Color3(.7, .7, .7);
                    spot.specular = new BABYLON.Color3(0, 0, 0);
                    spot.position.y = 20;
                    spot.position.x = 45;
              const moon = BABYLON.Mesh.CreateSphere("moon", 10, 4);
                    moon.material = new BABYLON.StandardMaterial("moon");
                    moon.material.emissiveColor = new BABYLON.Color3(1, 1, 0);  
                    moon.position.y = 20;
                    moon.position.x = 45;
              break;
            default:
              const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
                    light.intensity = .6;
              break;
          }
          break;
        default:
          break;
      }
    }
    sky_ground_water_setting(light_dark, degree, scene) {
      // 空・グランド・水を作成
      switch(true) {
        case !( light_dark == undefined && degree == undefined && scene == undefined ):      
          // 空を作成
          const skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size: 1000}, scene);
          const skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
                skyboxMaterial.backFaceCulling = false;
          const skyboxTexture = light_dark == 'dark'
                              ? "textures/sky_dark/skybox"
                              : "textures/sky/skybox";
                skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture(skyboxTexture, scene);
                skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
                skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
                skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
                skybox.material = skyboxMaterial;

          // グランドを作成
          const largeGroundMat = new BABYLON.StandardMaterial("largeGroundMat");
                largeGroundMat.diffuseTexture = new BABYLON.Texture("textures/ground/valleygrass.png");
          const largeGround = BABYLON.MeshBuilder.CreateGroundFromHeightMap(
                                "largeGround",
                                "textures/ground/villageheightmap.png",
                                { width:72, height:72, subdivisions: 200, minHeight:0, maxHeight: 8.5 }
                              );  
                largeGround.material = largeGroundMat;
                largeGround.rotation.y = degree(90);
                largeGround.position.y = -0.5;      

          // 水を作成
          const waterMesh = BABYLON.Mesh.CreateGround("waterMesh", 20, 37, 10, scene, false);
          const water = new BABYLON.WaterMaterial("water", scene, new BABYLON.Vector2(512, 512));
                water.backFaceCulling = true;
                water.bumpTexture = new BABYLON.Texture("textures/water/waterbump.png", scene);
                water.windForce = -2;
                water.waveHeight = 0.1;
                water.bumpHeight = 0.1;
                water.waterColor = new BABYLON.Color3(0.1, 0.1, 0.2);
                water.colorBlendFactor = 0.5;
                water.addToRenderList(skybox);
                water.addToRenderList(largeGround);
                waterMesh.material = water;
                waterMesh.position.x = 2.9;      
                waterMesh.position.y = 1.8;
                waterMesh.position.z = 16.5;
          break;
        default:
          break;
      }
    }
    tea_setting(scene) {
      // ティーを作成する
      switch(true) {
        case scene != undefined:
          const tea = BABYLON.Mesh.CreateSphere("tea", 10, .3);
                tea.material = new BABYLON.StandardMaterial("teamaterial");
                tea.material.diffuseColor = new BABYLON.Color3(1, 1, 1);  
                tea.position.y = 8.3;
                tea.position.x = -27.5;
                tea.position.z = 24;

          const tea1 = tea.clone();
                tea1.position.y = 8.3;
                tea1.position.x = -23.5;
                tea1.position.z = 28.5;

          const tea_under = BABYLON.MeshBuilder.CreateCylinder("tea_under", {height:.5, diameter: .1});
          const tea_under_material = new BABYLON.StandardMaterial("tea_under_material", scene);
                tea_under_material.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.8);
                tea_under_material.alpha = .9;
                tea_under.material = tea_under_material;
                tea_under.position.y = 8;
                tea_under.position.x = -27.5;
                tea_under.position.z = 24;
          const tea_under1 = tea_under.clone();
                tea_under1.position.x = -23.5;
                tea_under1.position.z = 28.5;
          break;
        default:
          null;        
      }
    }
    cup_setting(degree, scene) {
      // ゴルフカップを作成
      switch(true) {
        case !( degree == undefined && scene == undefined ):
          const golfcup = BABYLON.MeshBuilder.CreateCylinder("golfcup", {height:1, depth:0.1});
          const golfcupMaterial = new BABYLON.StandardMaterial("golfcupmaterial", scene);
                golfcupMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.8);
                golfcupMaterial.alpha = .9;
                golfcup.material = golfcupMaterial;
                golfcup.position.x = 32;
                golfcup.position.y = 2.12;
                golfcup.position.z = -10.8;
                golfcup.rotation.z = degree(2);
                golfcup.rotation.x = degree(-3);
          break;
        default:
          break;
      }
    }
    tree_setting(light_dark, scene) {
      // 木を作成する
      switch(true) {
        case !( light_dark == undefined && scene == undefined ):
          const elementree = light_dark == 'dark'
                           ? "textures/wood/wood_dark.png"
                           : "textures/wood/wood.png";
          const spritetree = new BABYLON.SpriteManager("treesManager", elementree, 135, {width: 538, height: 680}, scene); 

          for (let i = 0; i < 135; i++) {
            switch(true) {
              case i > 0 && i < 3:
                const treer_1 = new BABYLON.Sprite("tree", spritetree);
                treer_1.width = 4;
                treer_1.height = 6;
                treer_1.position.x = Math.random() * (1) - (30 + ((3 * 2 - i * 2 ) * 1.1 ));
                treer_1.position.z = Math.random() * -2  - (6 - ((3 * 2 - i * 2) * 1.3 ));
                treer_1.position.y = 5;
                break;    
。。。。(省略)
              default:
                const tree = new BABYLON.Sprite("tree", spritetree);
                tree.width = 4;
                tree.height = 6;
                tree.position.x = Math.random() * (-5) + 19;
                tree.position.z = Math.random() * 8 + 27;
                tree.position.y = 5;
                break;
            }
          }
          break;
        default:
          break;
      }
    }
    lump_setting(light_dark, scene, x, y, z) {
      // ランプを作成する
      switch(true) {
        case !( light_dark == undefined && scene == undefined ):       
          BABYLON.SceneLoader.ImportMeshAsync("", "", "lamp.babylon").then(() =>{
            const lignts = light_dark == 'dark' ? 0.63 : 0;
            const lampLight = new BABYLON.SpotLight("lampLight", BABYLON.Vector3.Zero(), new BABYLON.Vector3(0.25, -1, 0), lignts * Math.PI, 0.01, scene);
                  lampLight.diffuse = BABYLON.Color3.White();
                  lampLight.parent = scene.getMeshByName("bulb");

            const lamp = scene.getMeshByName("lamp");
                  lamp.position = new BABYLON.Vector3(x, y, z); 
                  lamp.rotation = BABYLON.Vector3.Zero();
                  lamp.rotation.y = -Math.PI / 2;

          });
          break;
        default:
          break;
      }
    }
    fountain_setting(scene) {
      // 噴水作成
      switch(true) {
        case scene != undefined:
          BABYLON.SceneLoader.ImportMeshAsync("", "", "fountain.babylon").then(() =>  {

            const fountain = scene.getMeshByName("fountain");
            fountain.position.x = -27.7;
            fountain.position.y = 2.3;
            fountain.position.z = 6.8;
            let particleSystem = new BABYLON.ParticleSystem("particles", 5000, scene);
            particleSystem.particleTexture = new BABYLON.Texture("textures/flare/flare.png", scene);
            particleSystem.emitter = new BABYLON.Vector3(-27.7, 6.3, 6.8);
            particleSystem.minEmitBox = new BABYLON.Vector3(0, 0, 0);
            particleSystem.maxEmitBox = new BABYLON.Vector3(0, 0, 0);
            particleSystem.color1 = new BABYLON.Color4(0.1, 0.1, 0.2, 0.8);
            particleSystem.color2 = new BABYLON.Color4(0.1, 0.1, 0.2, 0.8);
            particleSystem.colorDead = new BABYLON.Color4(0.1, 0.1, 0.2, 0.8);
            particleSystem.minSize = 0.1 / 2;
            particleSystem.maxSize = 0.8 / 2;
            particleSystem.minLifeTime = 1;
            particleSystem.maxLifeTime = 2.5;
            particleSystem.emitRate = 1500;
            particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ONEONE;
            particleSystem.gravity = new BABYLON.Vector3(0, -3.81, 0);
            particleSystem.direction1 = new BABYLON.Vector3(-.2, 1, .2);
            particleSystem.direction2 = new BABYLON.Vector3(.2, 1, -.2);
            particleSystem.minAngularSpeed = 0;
            particleSystem.maxAngularSpeed = Math.PI;
            particleSystem.minEmitPower = 2;
            particleSystem.maxEmitPower = 3;
            particleSystem.updateSpeed = 0.025;

            particleSystem.start();
          });  
          break;
        default:
          break;
      }    
    }
    createScene(degree, light_dark, engine, canvas) {
      // シーン作成
      switch(true) {
        case !( light_dark == undefined && degree == undefined ):
          // シーン・カメラ設定
          const scene = new BABYLON.Scene(engine);
          const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, Math.PI / 2.5, new BABYLON.Vector3(0, 0, 0));
          camera.position = new BABYLON.Vector3(30, 30, 30);
          camera.attachControl(canvas, true);

          // 光源設定
          this.pointerLight(light_dark, scene);

          // 空・グランド・水を作成
          this.sky_ground_water_setting(light_dark, this.degree, scene);

          // ティー作成
          this.tea_setting(scene);

          // カップを作成
          this.cup_setting(this.degree, scene);

          // 木を作成
          this.tree_setting(light_dark, scene);
              
          // ランプを作成
          this.lump_setting(light_dark, scene, 0, 2, -20);

          // 噴水を作成
          this.fountain_setting(scene);

          return scene;
          break;
        default:
          break;   
      }     
    };
  }
  new LightDarkmode();
</script>

上記のスクリプト処理のロジックについて解説します。

●初期表示のロジックは「ローディング画像を表示 -> 数秒後ローディング画像を非表示してグラフィックを表示」になってます。
●ライトモード or ダークモード切り替え時のロジックは「ローディング画像を表示 -> Canvasを削除 -> 新たにCanvasを生成 -> Babylon.jsで再描画 -> ローディング画像を非表示」になってます。

これで、ライトモード・ダークモード / 後背景の実装が出来ました。

カルーセル処理・画像クリック時拡大表示の実装

次にカルーセル処理・画像クリック時拡大表示するように実装します。

src/components/Card.astro
---
import { getNsports } from "../../library/microcms";
import Svg from '../components/Svg.astro';
const resnublog = await getNsports( { fields: ["id", "title", "image", "date"] } );
---

<div class="tw-relative">
  <div class="close_button tw-hidden tw-fixed tw-top-[74px] tw-right-[20px] tw-w-[30px] tw-text-[50px] tw-leading-[30px] tw-cursor-pointer tw-z-[10]">
    <Svg name="close"/>
  </div>
  <canvas
    id="renderCanvasList"
    class="tw-fixed tw-w-[100%] tw-h-[100%] tw-top-[0px] tw-left-[0px]"
    data-blog={resnublog.contents.map((content: any) => content.image.url + '|' + content.image.width + '|' + content.image.height  + '|' + content.title)}
  >
  </canvas> 
  <canvas
    id="renderCanvasBanner"
    class="tw-animate-card-fadein tw-animate-card-fadein-adjustment tw-hidden tw-fixed tw-w-[100%] tw-h-[100vh] tw-top-[0] tw-left-[0] tw-opacity-0">
  </canvas>
</div>

<script type="text/javascript">
  
  // カルーセル・背景動作切り替えスイッチ
  class MoveSwitch {
    constructor() {
      this.js_button = document.getElementsByClassName('js-caroucel-button');
      this.switch();
    }
    switch(isdata) {
      switch(true) {
        case isdata != undefined:
          switch(true) {
            case isdata.target.getAttribute('id') === 'background_switch':
              document.getElementById('caoucel_switch').classList.add('tw-opacity-50')
              document.getElementById('caoucel_switch').classList.remove('tw-filter-drow-shadow');
              document.getElementById('background_switch').classList.remove('tw-opacity-50')
              document.getElementById('background_switch').classList.add('tw-filter-drow-shadow');
              document.getElementById('renderCanvasList').classList.add('tw-pointer-events-none');
              document.getElementById('renderCanvasBanner').classList.add('tw-pointer-events-none');
              document.getElementById('renderCanvasList').classList.add('tw-opacity-70');
              document.getElementById('renderCanvasBanner').classList.add('!tw-opacity-70');
              break;
            default:
              document.getElementById('caoucel_switch').classList.remove('tw-opacity-50')
              document.getElementById('caoucel_switch').classList.add('tw-filter-drow-shadow');
              document.getElementById('background_switch').classList.add('tw-opacity-50')
              document.getElementById('background_switch').classList.remove('tw-filter-drow-shadow');
              document.getElementById('renderCanvasList').classList.remove('tw-pointer-events-none');
              document.getElementById('renderCanvasList').classList.remove('tw-opacity-70');
              document.getElementById('renderCanvasBanner').classList.remove('tw-pointer-events-none');
              document.getElementById('renderCanvasBanner').classList.remove('!tw-opacity-70');
              break;
          }      
          break;
        default:
          break;
      }
    }
  }
  const moveswitch = new MoveSwitch();
  for(let i = 0; i < moveswitch.js_button.length; i++){
    moveswitch.js_button[i].addEventListener("click",(e) => {
      moveswitch.switch(e);
    });
  }

  // フォトギャラリー一覧・詳細
  class Carousel {
    constructor() {
      this.degree();
      this.bannerlist();
      this.createScene();
      this.mainbanner();
    }
    degree(degrees) {
      // 度数を取得
      switch(true) {
        case degrees !== undefined:
          return degrees * (Math.PI / 180);
          break;
        default:
          return null;
          break;
      }
    }
    bannerlist() {
      // バナーリストを表示
      const canvasblog = document.getElementById('renderCanvasList');
      const engine = new BABYLON.Engine(canvasblog);  
      const scene = this.createScene(this.degree, this.mainbanner, engine, canvasblog);
      engine.runRenderLoop(() => {
        scene.render();
      });
      window.addEventListener('resize', () => {
        engine.resize();
      });
    }
    createScene(degree, mainbanner, engine, canvasblog) {
      switch(true) {
        case !( mainbanner == undefined && degree == undefined ):   
            // シーンを作成
            const scene = new BABYLON.Scene(engine);
            scene.clearColor = new BABYLON.Color4(0,0,0,0); 

            // カメラを作成
            const camera = new BABYLON.ArcRotateCamera("camera2", -Math.PI / 2,  Math.PI / 2, 20, BABYLON.Vector3.Zero(), scene);
            camera.attachControl(canvasblog, true);
            camera.useAutoRotationBehavior = true;
            camera.autoRotationBehavior.idleRotationSpeed = Math.PI * 10 / 180;  

            // ライトを作成
            const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0), scene);
            light.diffuse = new BABYLON.Color3(1, 1, 1);
            light.specular = new BABYLON.Color3(1, 1, 1);
            light.groundColor = new BABYLON.Color3(1, 1, 1);

            // ギャラリーリストを作成
            const canvaslist = canvasblog.dataset.blog;
            const canvaslistsplit = canvaslist.split(',');
            canvaslistsplit.forEach(function( value, index ) {
              const boxMat = new BABYLON.StandardMaterial("boxMat");
              const value_split = value.split('|');

              // 比率を計算する
              const hfix =  ( ( canvaslistsplit.length / 2.5 ) * value_split[2] ) / value_split[1];
              const wfix =  canvaslistsplit.length / 2.5;

              const box = BABYLON.MeshBuilder.CreatePlane("box", {height: hfix, width: wfix, sideOrientation: BABYLON.Mesh.DOUBLESIDE});
              box.rotation.y = degree( index * ( 360 / canvaslistsplit.length ) );

              box.position.x = - ( canvaslistsplit.length / 1.2 * Math.sin( degree( index * ( 360 / canvaslistsplit.length ) ) ) );
              box.position.y = 0;
              box.position.z = - ( canvaslistsplit.length / 1.2 * Math.cos( degree( index * ( 360 / canvaslistsplit.length ) ) ) );

              // テクスチャをはる
              boxMat.diffuseTexture = new BABYLON.Texture(value_split[0]);
              box.material = boxMat;

              // クリックイベントを作る
              box.actionManager = new BABYLON.ActionManager(scene);
              box.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnDoublePickTrigger, function () {
                const target = canvaslistsplit[index];
                const target_split = target.split('|');
                document.getElementById('renderCanvasList').classList.add('tw-hidden');
                document.getElementById('renderCanvasBanner').classList.remove('tw-hidden');
                document.querySelector('.close_button').classList.remove('tw-hidden');
                mainbanner(target_split[0], target_split[1], target_split[2]);
              }));
            });
            return scene;
          break;
        default:
          break;
      }
    }
    mainbanner(src, width, height) {

      // バナーレンダリング
      const canvasbanner = document.getElementById('renderCanvasBanner');
      const engine = new BABYLON.Engine(canvasbanner);  
      const detailScenes = function () {

        // シーンを作成
        const scene = new BABYLON.Scene(engine);
        scene.clearColor = new BABYLON.Color4(0,0,0,0); 

        // カメラを作成
        const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2,  Math.PI / 2, 18, BABYLON.Vector3.Zero(), scene);
        camera.attachControl(canvasbanner, true);

        // ライトを作成
        const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0), scene);
        light.diffuse = new BABYLON.Color3(1, 1, 1);
        light.specular = new BABYLON.Color3(1, 1, 1);
        light.groundColor = new BABYLON.Color3(1, 1, 1);      

        // ブロックを作成
        // マテリアルをセットする
        const boxMat = new BABYLON.StandardMaterial("boxMat");

        // ボックス型のジオメトリをセットする
        switch(true) {
          case String(width).length === 5 || String(height).length === 5:
            var wfix = width / 1000;
            var hfix = height / 1000;
            break;          
          case String(width).length === 4 || String(height).length === 4:
            var wfix = width / 100;
            var hfix = height / 100;
            break;
          case String(width).length === 3 || String(height).length === 3:
            var wfix = width / 10;
            var hfix = height / 10;            
            break;
          default:
            var wfix = width;
            var hfix = height;
            break;  
        }
        // SP・PC判定処理
        switch(true) {
          case window.matchMedia('(max-width:780px)').matches:
            const spwfix = window.innerWidth;
            const sphfix = (window.innerWidth * hfix ) / wfix;
            wfix = spwfix / 50;
            hfix = sphfix / 50;
            break;
          default:
            break;            
        }
        const box = BABYLON.MeshBuilder.CreatePlane("box", {height: hfix, width: wfix, sideOrientation: BABYLON.Mesh.DOUBLESIDE});
        // テクスチャをセットする
        if( src !== null ) {
            boxMat.diffuseTexture = new BABYLON.Texture(src);
            box.material = boxMat;
            return scene;
        }
      };
      const scene = detailScenes();
      engine.runRenderLoop(() => {
        scene.render();
      });
      window.addEventListener('resize', () => {
        engine.resize();
      });
    }
  }
  const carousel = new Carousel();

  document.querySelector('.close_button').addEventListener("click",(e) => { 
    document.getElementById('renderCanvasBanner').classList.add('tw-hidden');
    document.getElementById('renderCanvasList').classList.remove('tw-hidden');
    document.querySelector('.close_button').classList.add('tw-hidden');
  });

</script>

上記のスクリプト処理のロジックについて解説します。

●カルーセル一覧・拡大表示とカルーセル・後背景のマウスムーブの切り替えスイッチとそれぞれのClass関数で実行しております。
●カルーセル・後背景のマウスムーブの切り替えスイッチは至って単純でCSSのclass操作で実行しております。
●カルーセル一覧・拡大表示に関してはrenderCanvasListのCanvasにdata-blog属性を作成してresnublogのデータをmap関数で配列を回してセットして、JavaScriptでその情報を取得してカルーセル表示しております。
●クリック時の拡大表示に関しては対象のインデックス値を利用して、拡大ようの関数mainbannerにその画像・サイズなどを引数として渡して、renderCanvasBannerのCanvasにレンダリングして拡大表示しております。

それぞれ実行して以下のように表示して完成しました。

GitHubは以下になります。ヘッダー・SVGなど細かいところに関してはこちらでご確認ください。
https://github.com/uemura5683/n-sports

LTについて

また、前月にLTを登壇しましてその資料を作成しましたのでこちらも合わせてご確認ください。

https://www.figma.com/deck/7zTaT9Z7dTjshhTLygnisu/microCMS-×Babylon.jsで-フォトギャラリーサイトを作った?node-id=1-34

感想

今回はBabylon.js × microCMSでフォトギャラリーを作ってみて試行錯誤はありましたがなんとか出来ました。反省点としてはフレームワークはAstroとBabylon.jsは相関性があまりなく、それだったらフルスクラッチ状でも実装できたなと思いました。
次は自分のポートフォリオサイトを大幅にリニューアルしていきたいと思います。

Discussion