{
  "$mulmocast": {
    "version": "1.1"
  },
  "lang": "ja",
  "title": "Three.js GLB Demo",
  "canvasSize": {
    "width": 1280,
    "height": 720
  },
  "audioParams": {
    "padding": 0,
    "introPadding": 0,
    "closingPadding": 0,
    "outroPadding": 0
  },
  "beats": [
    {
      "id": "glb_preview",
      "duration": 16,
      "text": "GLBモデルのプレビューです。",
      "image": {
        "type": "html_tailwind",
        "html": [
          "<div id='three-container' style='position:fixed;inset:0;overflow:hidden;background:#0d1224'>",
          "  <canvas id='c' style='display:block;width:100%;height:100%'></canvas>",
          "</div>"
        ],
        "script": [
          "import * as THREE from 'https://esm.sh/three@0.182.0';",
          "import { GLTFLoader } from 'https://esm.sh/three@0.182.0/examples/jsm/loaders/GLTFLoader.js';",
          "const canvas = document.getElementById('c');",
          "const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });",
          "renderer.setClearColor(0x0d1224);",
          "renderer.outputColorSpace = THREE.SRGBColorSpace;",
          "const scene = new THREE.Scene();",
          "const camera = new THREE.PerspectiveCamera(45, 16 / 9, 0.01, 1000);",
          "camera.position.set(0, 1.2, 7.0);",
          "camera.lookAt(0, 0.9, 0);",
          "camera.zoom = 1.0;",
          "camera.updateProjectionMatrix();",
          "scene.add(new THREE.AmbientLight(0xffffff, 1.2));",
          "const key = new THREE.DirectionalLight(0xffffff, 1.4);",
          "key.position.set(3, 5, 2);",
          "scene.add(key);",
          "const rim = new THREE.DirectionalLight(0x88aaff, 0.8);",
          "rim.position.set(-4, 2, -3);",
          "scene.add(rim);",
          "const grid = new THREE.GridHelper(10, 20, 0x444466, 0x222244);",
          "grid.position.y = -0.01;",
          "scene.add(grid);",
          "let moveRoot = null;",
          "let turnRoot = null;",
          "let modelOffset = null;",
          "let modelReadyResolved = false;",
          "let resolveModelReady;",
          "const modelReady = new Promise((resolve) => { resolveModelReady = resolve; });",
          "const modelUrl = 'glb/sample_2026-03-15T172907.296_compat.glb';",
          "const loader = new GLTFLoader();",
          "loader.load(modelUrl, (gltf) => {",
          "  moveRoot = new THREE.Group();",
          "  turnRoot = new THREE.Group();",
          "  modelOffset = new THREE.Group();",
          "  moveRoot.add(turnRoot);",
          "  turnRoot.add(modelOffset);",
          "  modelOffset.add(gltf.scene);",
          "  scene.add(moveRoot);",
          "  const box = new THREE.Box3().setFromObject(modelOffset);",
          "  const size = box.getSize(new THREE.Vector3());",
          "  const maxDim = Math.max(size.x, size.y, size.z) || 1;",
          "  const scale = 1.8 / maxDim;",
          "  modelOffset.scale.setScalar(scale);",
          "  box.setFromObject(modelOffset);",
          "  const centered = box.getCenter(new THREE.Vector3());",
          "  modelOffset.position.sub(centered);",
          "  modelOffset.position.y += 0.9;",
          "  modelReadyResolved = true;",
          "  resolveModelReady();",
          "}, undefined, (error) => {",
          "  console.error('GLB load failed:', error);",
          "  modelReadyResolved = true;",
          "  resolveModelReady();",
          "});",
          "let lastW = 0;",
          "let lastH = 0;",
          "const syncSize = () => {",
          "  const w = Math.max(1, Math.floor(window.innerWidth || document.documentElement.clientWidth));",
          "  const h = Math.max(1, Math.floor(window.innerHeight || document.documentElement.clientHeight));",
          "  if (w !== lastW || h !== lastH) {",
          "    renderer.setSize(w, h, true);",
          "    camera.aspect = w / h;",
          "    camera.updateProjectionMatrix();",
          "    lastW = w;",
          "    lastH = h;",
          "  }",
          "};",
          "const lerp = (a, b, u) => a + (b - a) * u;",
          "const clamp01 = (v) => Math.max(0, Math.min(1, v));",
          "const xMin = -1.6;",
          "const xMax = 1.6;",
          "const moveEnd = 0.45;",
          "const turnEnd = 0.55;",
          "const turnFrontEnd = moveEnd + (turnEnd - moveEnd) * 0.75;",
          "const faceRight = Math.PI / 2;",
          "const faceFront = 0;",
          "const faceLeft = -Math.PI / 2;",
          "const holdFrontSeconds = 3;",
          "window.render = async function render(frame, total, fps) {",
          "  if (!modelReadyResolved) {",
          "    await modelReady;",
          "  }",
          "  document.body.style.zoom = '1';",
          "  syncSize();",
          "  const t = frame / Math.max(1, total - 1);",
          "  camera.zoom = lerp(1.0, 1.18, t);",
          "  camera.updateProjectionMatrix();",
          "  const fpsSafe = fps || 30;",
          "  const holdFrontSpan = (holdFrontSeconds * fpsSafe) / Math.max(1, total - 1);",
          "  const holdFrontEnd = Math.min(0.97, turnFrontEnd + holdFrontSpan);",
          "  const rotateLeftSpan = Math.max(0.02, turnEnd - turnFrontEnd);",
          "  const rotateLeftEnd = Math.min(0.985, holdFrontEnd + rotateLeftSpan);",
          "  if (moveRoot && turnRoot) {",
          "    if (t < moveEnd) {",
          "      const u = clamp01(t / moveEnd);",
          "      moveRoot.position.x = lerp(xMin, xMax, u);",
          "      turnRoot.rotation.y = faceRight;",
          "    } else if (t < turnFrontEnd) {",
          "      const u = clamp01((t - moveEnd) / Math.max(1e-6, turnFrontEnd - moveEnd));",
          "      moveRoot.position.x = xMax;",
          "      turnRoot.rotation.y = lerp(faceRight, faceFront, u);",
          "    } else if (t < holdFrontEnd) {",
          "      moveRoot.position.x = xMax;",
          "      turnRoot.rotation.y = faceFront;",
          "    } else if (t < rotateLeftEnd) {",
          "      const u = clamp01((t - holdFrontEnd) / Math.max(1e-6, rotateLeftEnd - holdFrontEnd));",
          "      moveRoot.position.x = xMax;",
          "      turnRoot.rotation.y = lerp(faceFront, faceLeft, u);",
          "    } else {",
          "      const u = clamp01((t - rotateLeftEnd) / Math.max(1e-6, 1 - rotateLeftEnd));",
          "      moveRoot.position.x = lerp(xMax, xMin, u);",
          "      turnRoot.rotation.y = faceLeft;",
          "    }",
          "  }",
          "  camera.lookAt(0, 0.9, 0);",
          "  renderer.render(scene, camera);",
          "};"
        ],
        "animation": {
          "fps": 30
        }
      }
    }
  ]
}
