{"id":111,"date":"2026-05-16T06:14:01","date_gmt":"2026-05-16T06:14:01","guid":{"rendered":"https:\/\/hypermuseum.org\/?post_type=raum&#038;p=111"},"modified":"2026-05-16T06:14:02","modified_gmt":"2026-05-16T06:14:02","slug":"3-it-belongs-in-a-museum","status":"publish","type":"raum","link":"https:\/\/hypermuseum.org\/?raum=3-it-belongs-in-a-museum","title":{"rendered":"#3 It belongs in a museum!"},"content":{"rendered":"\n<!doctype html>\n<html lang=\"de\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\" \/>\n  <title>HYPERMUSEUM<\/title>\n  <style>\n    :root{\n      --muted: rgba(0,0,0,.52);\n      --bd: rgba(0,0,0,.14);\n      --sh: 0 10px 30px rgba(0,0,0,.08);\n      --glass: rgba(255,255,255,.90);\n    }\n    html, body{\n      margin:0; height:100%; overflow:hidden;\n      background:#fff;\n      font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;\n    }\n    #stage{ position:fixed; inset:0; }\n    canvas{ width:100vw; height:100vh; display:block; touch-action:none; }\n\n    .brand{\n      position:fixed; left:16px; top:14px; z-index:20;\n      font-size:16px; font-weight:860; letter-spacing:.02em;\n      color:#111; text-transform:uppercase; user-select:none;\n      pointer-events:none;\n    }\n    .cycleWord{\n      position:fixed; right:16px; top:14px; z-index:20;\n      font-size:12px; font-weight:760; letter-spacing:.10em;\n      color: var(--muted); text-transform:uppercase; user-select:none;\n      pointer-events:none;\n      min-height: 1.2em;\n    }\n\n    \/* slim bottom bar only *\/\n    .bar{\n      position:fixed;\n      left:14px;\n      right:14px;\n      bottom:14px;\n      z-index:30;\n      pointer-events:none;\n    }\n    .barInner{\n      pointer-events:auto;\n      background: var(--glass);\n      border:1px solid var(--bd);\n      box-shadow: var(--sh);\n      backdrop-filter: blur(10px);\n      -webkit-backdrop-filter: blur(10px);\n      padding: 10px;\n      display:flex;\n      align-items:center;\n      justify-content:space-between;\n      gap:12px;\n    }\n    .btnRow{ display:flex; gap:10px; align-items:center; }\n    .btn{\n      appearance:none;\n      border:1px solid rgba(0,0,0,.28);\n      background: transparent;\n      color:#111;\n      padding: 12px 16px;\n      font-size:14px;\n      font-weight:820;\n      letter-spacing:.06em;\n      cursor:pointer;\n      text-transform:uppercase;\n      user-select:none;\n      white-space:nowrap;\n    }\n    .btn:active{ transform: translateY(1px); }\n\n    .inputWrap{\n      display:flex;\n      align-items:center;\n      gap:10px;\n      flex: 1;\n      min-width: 180px;\n      max-width: 520px;\n    }\n    .inputHint{\n      font-size:12px;\n      color: var(--muted);\n      letter-spacing:.06em;\n      text-transform:uppercase;\n      user-select:none;\n      white-space:nowrap;\n    }\n    input#textInput{\n      width:100%;\n      border: 1px solid rgba(0,0,0,.22);\n      background: rgba(255,255,255,.78);\n      padding: 10px 10px;\n      font-size:14px;\n      font-weight:720;\n      line-height:1.2;\n      outline:none;\n      color:#111;\n      caret-color:#111;\n      font-family: \"Helvetica Neue\", Helvetica, Arial, system-ui, sans-serif;\n    }\n    input#textInput:focus{ border-color: rgba(0,0,0,.42); }\n\n    .hint{\n      font-size:12px;\n      color: var(--muted);\n      letter-spacing:.06em;\n      text-transform:uppercase;\n      user-select:none;\n      text-align:right;\n      white-space:nowrap;\n    }\n\n    \/* small label toast *\/\n    .toast{\n      position:fixed;\n      left:14px;\n      bottom:74px;\n      z-index:25;\n      pointer-events:none;\n      background: rgba(255,255,255,.88);\n      border:1px solid var(--bd);\n      box-shadow: var(--sh);\n      backdrop-filter: blur(10px);\n      -webkit-backdrop-filter: blur(10px);\n      padding: 10px 12px;\n      color:#111;\n      font-size:12px;\n      font-weight:750;\n      letter-spacing:.06em;\n      text-transform:uppercase;\n      opacity:0;\n      transform: translateY(8px);\n      transition: opacity 160ms ease, transform 160ms ease;\n      max-width: min(520px, calc(100vw - 28px));\n    }\n    .toast.show{\n      opacity:1;\n      transform: translateY(0);\n    }\n\n    @media (max-width: 720px){\n      .barInner{\n        flex-wrap:wrap;\n        justify-content:flex-start;\n      }\n      .hint{\n        width:100%;\n        text-align:left;\n        white-space:normal;\n        line-height:1.25;\n        padding-top:4px;\n      }\n      .inputWrap{\n        max-width: none;\n        width: 100%;\n        order: 3;\n      }\n    }\n    @media (max-width: 560px){\n      .btn{ padding: 10px 12px; font-size: 12px; }\n      input#textInput{ font-size: 13px; }\n      .inputHint{ font-size: 11px; }\n    }\n  <\/style>\n<\/head>\n<body>\n  <div id=\"stage\"><\/div>\n\n  <div class=\"brand\">HYPERMUSEUM<\/div>\n  <div class=\"cycleWord\" id=\"cycleWord\"><\/div>\n\n  <div class=\"toast\" id=\"toast\">TEXT WALL<\/div>\n\n  <div class=\"bar\">\n    <div class=\"barInner\">\n      <div class=\"btnRow\">\n        <button class=\"btn\" id=\"viewsBtn\" type=\"button\">VIEWS<\/button>\n        <button class=\"btn\" id=\"exitBtn\" type=\"button\">EXIT<\/button>\n      <\/div>\n\n      <div class=\"inputWrap\">\n        <div class=\"inputHint\">Enter your Text<\/div>\n        <input id=\"textInput\" value=\"hypermuseum\" spellcheck=\"false\" \/>\n      <\/div>\n\n      <div class=\"hint\">drag = look&nbsp;&nbsp; pinch\/wheel = zoom&nbsp;&nbsp; tap\/click = focus<\/div>\n    <\/div>\n  <\/div>\n\n  <script src=\"https:\/\/unpkg.com\/three@0.160.0\/build\/three.min.js\"><\/script>\n  <script>\n  (function(){\n    const stage = document.getElementById(\"stage\");\n    const textInput = document.getElementById(\"textInput\");\n    const toast = document.getElementById(\"toast\");\n\n    function showToast(text){\n      toast.textContent = text;\n      toast.classList.add(\"show\");\n      clearTimeout(showToast._t);\n      showToast._t = setTimeout(()=>toast.classList.remove(\"show\"), 1800);\n    }\n\n    const renderer = new THREE.WebGLRenderer({ antialias:true, alpha:false, powerPreference:\"high-performance\" });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n    renderer.setClearColor(0xffffff, 1);\n    renderer.shadowMap.enabled = true;\n    renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n    renderer.toneMapping = THREE.ACESFilmicToneMapping;\n    renderer.toneMappingExposure = 1.12;\n    renderer.outputColorSpace = THREE.SRGBColorSpace;\n    renderer.physicallyCorrectLights = true;\n    stage.appendChild(renderer.domElement);\n\n    const scene = new THREE.Scene();\n    const camera = new THREE.PerspectiveCamera(52, 1, 0.05, 250);\n\n    function resize(){\n      const w = window.innerWidth, h = window.innerHeight;\n      renderer.setSize(w, h, false);\n      camera.aspect = w \/ h;\n      camera.updateProjectionMatrix();\n    }\n    addEventListener(\"resize\", resize);\n    resize();\n\n    function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }\n    function lerp(a,b,t){ return a + (b-a) * t; }\n    function easeInOutCubic(t){ return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3)\/2; }\n\n    const ROOM_W = 18;\n    const ROOM_D = 14;\n    const ROOM_H = 6;\n\n    const room = new THREE.Group();\n    scene.add(room);\n\n    const floorMat = new THREE.MeshStandardMaterial({ color: 0xeeeeee, roughness: 0.98, metalness: 0.0 });\n    const floor = new THREE.Mesh(new THREE.PlaneGeometry(ROOM_W, ROOM_D), floorMat);\n    floor.rotation.x = -Math.PI\/2;\n    floor.receiveShadow = true;\n    room.add(floor);\n\n    function edgesBox(w,h,d, color=0x2a2a2a, opacity=0.55){\n      const g = new THREE.BoxGeometry(w,h,d);\n      const e = new THREE.EdgesGeometry(g, 12);\n      const m = new THREE.LineBasicMaterial({ color, transparent:true, opacity });\n      const l = new THREE.LineSegments(e, m);\n      return l;\n    }\n    const roomEdges = edgesBox(ROOM_W, ROOM_H, ROOM_D, 0x2a2a2a, 0.55);\n    roomEdges.position.set(0, ROOM_H\/2, 0);\n    room.add(roomEdges);\n\n    function outlineEdges(mesh, opacity=0.22){\n      const e = new THREE.EdgesGeometry(mesh.geometry, 14);\n      const m = new THREE.LineBasicMaterial({ color: 0x2a2a2a, transparent:true, opacity });\n      const l = new THREE.LineSegments(e, m);\n      l.position.copy(mesh.position);\n      l.rotation.copy(mesh.rotation);\n      l.scale.copy(mesh.scale);\n      return l;\n    }\n\n    const ambient = new THREE.AmbientLight(0xffffff, 0.95);\n    scene.add(ambient);\n\n    const key = new THREE.DirectionalLight(0xffffff, 2.2);\n    key.position.set(8, 14, 8);\n    key.castShadow = true;\n    key.shadow.mapSize.set(2048, 2048);\n    key.shadow.camera.near = 1;\n    key.shadow.camera.far = 60;\n    key.shadow.camera.left = -18;\n    key.shadow.camera.right = 18;\n    key.shadow.camera.top = 18;\n    key.shadow.camera.bottom = -18;\n    key.shadow.bias = -0.0002;\n    scene.add(key);\n\n    const fill = new THREE.DirectionalLight(0xffffff, 1.25);\n    fill.position.set(-10, 10, -8);\n    scene.add(fill);\n\n    const glassMat = new THREE.MeshPhysicalMaterial({\n      color: 0xffffff,\n      roughness: 0.03,\n      metalness: 0.0,\n      transmission: 0.985,\n      thickness: 0.35,\n      ior: 1.52,\n      transparent: true,\n      opacity: 1.0,\n      envMapIntensity: 1.15\n    });\n    const blackMat = new THREE.MeshStandardMaterial({ color: 0x0f0f0f, roughness: 0.85, metalness: 0.0 });\n    const plateMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.98, metalness: 0.0 });\n\n    function makeVitrine(w, d, glassH){\n      const g = new THREE.Group();\n\n      const baseH = 0.58;\n      const base = new THREE.Mesh(new THREE.BoxGeometry(w, baseH, d), blackMat);\n      base.position.y = baseH\/2;\n      base.castShadow = true;\n      base.receiveShadow = true;\n      g.add(base);\n      g.add(outlineEdges(base, 0.28));\n\n      const plateH = 0.12;\n      const plate = new THREE.Mesh(new THREE.BoxGeometry(w*0.965, plateH, d*0.965), plateMat);\n      plate.position.y = baseH + plateH\/2;\n      plate.castShadow = true;\n      plate.receiveShadow = true;\n      g.add(plate);\n      g.add(outlineEdges(plate, 0.20));\n\n      const glass = new THREE.Mesh(new THREE.BoxGeometry(w*0.992, glassH, d*0.992), glassMat);\n      glass.position.y = baseH + glassH\/2 + plateH;\n      glass.castShadow = true;\n      g.add(glass);\n      g.add(outlineEdges(glass, 0.22));\n\n      return g;\n    }\n\n    const vitrine = makeVitrine(4.8, 2.6, 1.55);\n    vitrine.position.set(-5.0, 0, -2.0);\n    vitrine.rotation.y = 0.10;\n    room.add(vitrine);\n\n    const plinthMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.98, metalness: 0.0 });\n    const plinth = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.8, 0.8), plinthMat);\n    plinth.position.set(-6.2, 0.4, 2.2);\n    plinth.castShadow = true;\n    plinth.receiveShadow = true;\n    room.add(plinth);\n    room.add(outlineEdges(plinth, 0.20));\n\n    const texLoader = new THREE.TextureLoader();\n    texLoader.crossOrigin = \"anonymous\";\n    const WALL_IMAGE_URL = \"https:\/\/i0.wp.com\/christophbalzar.com\/wp-content\/uploads\/2023\/03\/wAdJV_400.webp?w=400&#038;ssl=1\";\n\n    const imagePlane = new THREE.Mesh(\n      new THREE.PlaneGeometry(3.9, 4.6),\n      new THREE.MeshBasicMaterial({ color: 0xffffff })\n    );\n    imagePlane.position.set(ROOM_W\/2 - 0.02, ROOM_H\/2, -1.2);\n    imagePlane.rotation.y = -Math.PI\/2;\n    imagePlane.userData.name = \"wall-image\";\n    room.add(imagePlane);\n\n    texLoader.load(WALL_IMAGE_URL, (tex)=>{\n      tex.colorSpace = THREE.SRGBColorSpace;\n      tex.anisotropy = 16;\n      tex.minFilter = THREE.LinearMipmapLinearFilter;\n      tex.magFilter = THREE.LinearFilter;\n\n      const img = tex.image;\n      const aspect = (img && img.width && img.height) ? (img.width \/ img.height) : 1;\n\n      const h = 4.6;\n      const w = h * aspect;\n      imagePlane.geometry.dispose();\n      imagePlane.geometry = new THREE.PlaneGeometry(w, h);\n      imagePlane.material.map = tex;\n      imagePlane.material.needsUpdate = true;\n    });\n\n    const textWallZ = ROOM_D\/2 - 0.02;\n\n    const textWallPlane = new THREE.Mesh(\n      new THREE.PlaneGeometry(ROOM_W, ROOM_H),\n      new THREE.MeshBasicMaterial({ color: 0xffffff })\n    );\n    textWallPlane.position.set(0, ROOM_H\/2, textWallZ);\n    textWallPlane.rotation.y = Math.PI;\n    textWallPlane.userData.name = \"text-wall\";\n    room.add(textWallPlane);\n\n    const textCanvas = document.createElement(\"canvas\");\n    textCanvas.width = 4096;\n    textCanvas.height = 2048;\n    const tctx = textCanvas.getContext(\"2d\");\n\n    const textTex = new THREE.CanvasTexture(textCanvas);\n    textTex.colorSpace = THREE.SRGBColorSpace;\n    textTex.anisotropy = 8;\n    textTex.minFilter = THREE.LinearMipmapLinearFilter;\n    textTex.magFilter = THREE.LinearFilter;\n\n    textWallPlane.material.map = textTex;\n    textWallPlane.material.needsUpdate = true;\n\n    function splitLines(str){ return String(str).replace(\/\\r\/g, \"\").split(\"\\n\"); }\n\n    function wrapLine(line, maxWidthPx, fontPx){\n      tctx.font = `900 ${fontPx}px \"Helvetica Neue\", Helvetica, Arial, sans-serif`;\n      const words = line.split(\/(\\s+)\/).filter(s=>s.length);\n      const out = [];\n      let cur = \"\";\n      const widthOf = (s)=>tctx.measureText(s).width;\n\n      for(const w of words){\n        if(w.trim().length === 0){ cur += w; continue; }\n        const test = cur ? (cur + w) : w;\n        if(widthOf(test) <= maxWidthPx){ cur = test; continue; }\n        if(cur) out.push(cur.trimEnd());\n        cur = w;\n\n        if(widthOf(cur) > maxWidthPx){\n          const chars = cur.split(\"\");\n          let chunk = \"\";\n          for(const ch of chars){\n            const test2 = chunk + ch;\n            if(widthOf(test2) <= maxWidthPx){ chunk = test2; }\n            else{ if(chunk) out.push(chunk); chunk = ch; }\n          }\n          cur = chunk;\n        }\n      }\n      if(cur) out.push(cur.trimEnd());\n      return out;\n    }\n\n    function layoutText(text){\n      const W = textCanvas.width;\n      const H = textCanvas.height;\n\n      tctx.clearRect(0,0,W,H);\n      tctx.fillStyle = \"#ffffff\";\n      tctx.fillRect(0,0,W,H);\n\n      const raw = String(text || \"\");\n      const content = raw.trim().length ? raw : \"hypermuseum\";\n\n      const padding = 0;\n      const maxW = W - padding*2;\n      const maxH = H - padding*2;\n\n      let lo = 12, hi = 560;\n      let bestFont = 64;\n      let bestLines = [\"hypermuseum\"];\n      const lh = 1.02;\n\n      for(let iter=0; iter<18; iter++){\n        const mid = Math.floor((lo+hi)\/2);\n        const outLines = [];\n\n        for(const line of splitLines(content)){\n          if(line.length === 0){ outLines.push(\"\"); continue; }\n          const wrapped = wrapLine(line, maxW, mid);\n          for(const w of wrapped) outLines.push(w);\n        }\n\n        const totalH = outLines.length * (mid * lh);\n        let fits = totalH <= maxH;\n\n        if(fits){\n          tctx.font = `900 ${mid}px \"Helvetica Neue\", Helvetica, Arial, sans-serif`;\n          for(const L of outLines){\n            if(tctx.measureText(L).width > maxW + 0.5){ fits = false; break; }\n          }\n        }\n\n        if(fits){\n          bestFont = mid;\n          bestLines = outLines;\n          lo = mid + 1;\n        }else{\n          hi = mid - 1;\n        }\n      }\n\n      tctx.fillStyle = \"#ff0000\";\n      tctx.textBaseline = \"alphabetic\";\n      tctx.textAlign = \"left\";\n      tctx.font = `900 ${bestFont}px \"Helvetica Neue\", Helvetica, Arial, sans-serif`;\n\n      const lineH = bestFont * lh;\n      const blockH = bestLines.length * lineH;\n      const yStart = padding + (maxH - blockH)\/2 + bestFont;\n\n      for(let i=0; i<bestLines.length; i++){\n        const L = bestLines[i];\n        const w = tctx.measureText(L).width;\n        const x = padding + (maxW - w)\/2;\n        const y = yStart + i * lineH;\n        tctx.fillText(L, x, y);\n      }\n\n      textTex.needsUpdate = true;\n    }\n\n    layoutText(textInput.value);\n\n    let raf = 0;\n    textInput.addEventListener(\"input\", ()=>{\n      if(raf) return;\n      raf = requestAnimationFrame(()=>{ raf = 0; layoutText(textInput.value); });\n    });\n\n    const raycaster = new THREE.Raycaster();\n    const pointer = new THREE.Vector2();\n\n    function setPointer(e){\n      const r = renderer.domElement.getBoundingClientRect();\n      const x = (e.clientX - r.left) \/ r.width;\n      const y = (e.clientY - r.top) \/ r.height;\n      pointer.x = x * 2 - 1;\n      pointer.y = -(y * 2 - 1);\n    }\n\n    function pickObject(e){\n      setPointer(e);\n      raycaster.setFromCamera(pointer, camera);\n      const hits = raycaster.intersectObjects([textWallPlane, imagePlane, vitrine, plinth], true);\n      return hits.length ? hits[0].object : null;\n    }\n\n    function focusTextWall(){\n      targetLook.set(0, ROOM_H\/2, textWallZ - 0.02);\n      targetRotY = Math.PI;\n      targetRotX = -0.12;\n      distance = 15.6;\n      showToast(\"TEXT WALL\");\n    }\n\n    let distance = 15.6;\n    const minDist = 7.0, maxDist = 34.0;\n\n    let rotX = -0.12, rotY = Math.PI;\n    let targetRotX = rotX, targetRotY = rotY;\n\n    const lookTarget = new THREE.Vector3(0, ROOM_H\/2, textWallZ - 0.02);\n    const targetLook = lookTarget.clone();\n\n    let isDragging = false;\n    let lastX = 0, lastY = 0;\n\n    let userHoldUntil = 0;\n    function markUser(){ userHoldUntil = performance.now() + 2400; }\n\n    renderer.domElement.addEventListener(\"pointerdown\", (e)=>{\n      isDragging = true;\n      lastX = e.clientX;\n      lastY = e.clientY;\n      markUser();\n      try{ renderer.domElement.setPointerCapture(e.pointerId); }catch(_){}\n    });\n\n    renderer.domElement.addEventListener(\"pointermove\", (e)=>{\n      if(!isDragging) return;\n      const dx = e.clientX - lastX;\n      const dy = e.clientY - lastY;\n      lastX = e.clientX;\n      lastY = e.clientY;\n\n      targetRotY += dx * 0.006;\n      targetRotX += dy * 0.006;\n      targetRotX = clamp(targetRotX, -0.78, 0.24);\n      markUser();\n    });\n\n    renderer.domElement.addEventListener(\"pointerup\", (e)=>{\n      isDragging = false;\n      try{ renderer.domElement.releasePointerCapture(e.pointerId); }catch(_){}\n    });\n\n    renderer.domElement.addEventListener(\"wheel\", (e)=>{\n      e.preventDefault();\n      distance = clamp(distance + e.deltaY * 0.004, minDist, maxDist);\n      markUser();\n    }, { passive:false });\n\n    let tapStart = null;\n    renderer.domElement.addEventListener(\"pointerdown\", (e)=>{\n      tapStart = { x:e.clientX, y:e.clientY, t: performance.now() };\n    });\n\n    renderer.domElement.addEventListener(\"pointerup\", (e)=>{\n      if(!tapStart) return;\n      const dt = performance.now() - tapStart.t;\n      const dx = e.clientX - tapStart.x;\n      const dy = e.clientY - tapStart.y;\n      const isTap = dt < 260 &#038;&#038; Math.hypot(dx,dy) < 8;\n      tapStart = null;\n      if(!isTap) return;\n\n      const obj = pickObject(e);\n      if(!obj) return;\n\n      markUser();\n\n      const n = obj.userData &#038;&#038; obj.userData.name;\n      if(n === \"text-wall\"){ focusTextWall(); return; }\n      if(n === \"wall-image\"){\n        targetLook.set(ROOM_W\/2 - 0.02, ROOM_H\/2, -1.2);\n        targetRotY = Math.PI - 1.10;\n        targetRotX = -0.18;\n        distance = 16.4;\n        showToast(\"WALL IMAGE\");\n        return;\n      }\n\n      targetLook.copy(new THREE.Box3().setFromObject(obj).getCenter(new THREE.Vector3()));\n      showToast(\"OBJECT\");\n    });\n\n    function view(rx, ry, dist, lx, ly, lz, name){\n      return { rx, ry, dist, look: new THREE.Vector3(lx,ly,lz), name };\n    }\n\n    \/* add more total views *\/\n    const views = [\n      view(-0.10, Math.PI,       18.0,  0.0, ROOM_H\/2, textWallZ - 0.02, \"TEXT WALL TOTAL\"),\n      view(-0.08, Math.PI,       12.4,  0.0, ROOM_H\/2, textWallZ - 0.02, \"TEXT WALL CLOSE\"),\n      view(-0.16, Math.PI+0.36,  20.0,  0.0, ROOM_H\/2, textWallZ - 0.02, \"ANGLE LEFT TOTAL\"),\n      view(-0.16, Math.PI-0.36,  20.0,  0.0, ROOM_H\/2, textWallZ - 0.02, \"ANGLE RIGHT TOTAL\"),\n      view(-0.14, Math.PI+0.14,  24.0,  0.0, 2.9,     0.0,               \"ROOM TOTAL\"),\n      view(-0.20, Math.PI-1.10,  18.0,  ROOM_W\/2 - 0.02, ROOM_H\/2, -1.2,  \"IMAGE TOTAL\"),\n      view(-0.18, Math.PI-1.10,  13.8,  ROOM_W\/2 - 0.02, ROOM_H\/2, -1.2,  \"IMAGE CLOSE\")\n    ];\n\n    let tourOn = true;\n    let tourIndex = 0;\n\n    let tourT0 = performance.now();\n    let tourDwellMs = 3200;\n    let tourMoveMs = 6200;\n\n    let fromView = views[0];\n    let toView = views[0];\n\n    function startLeg(nextIndex){\n      fromView = view(targetRotX, targetRotY, distance, targetLook.x, targetLook.y, targetLook.z, \"FROM\");\n      toView = views[nextIndex];\n      tourIndex = nextIndex;\n      tourT0 = performance.now();\n      tourDwellMs = 3200;\n      tourMoveMs = 6200;\n      showToast(toView.name);\n    }\n    function advance(){\n      const next = (tourIndex + 1) % views.length;\n      startLeg(next);\n    }\n    startLeg(0);\n\n    function updateTour(now){\n      if(!tourOn) return;\n      if(now < userHoldUntil) return;\n\n      const dt = now - tourT0;\n      if(dt < tourDwellMs) return;\n\n      const moveT = clamp((dt - tourDwellMs) \/ tourMoveMs, 0, 1);\n      const s = easeInOutCubic(moveT);\n\n      targetRotX = lerp(fromView.rx, toView.rx, s);\n      targetRotY = lerp(fromView.ry, toView.ry, s);\n      distance   = lerp(fromView.dist, toView.dist, s);\n      targetLook.lerpVectors(fromView.look, toView.look, s);\n\n      if(moveT >= 1){\n        tourT0 = now;\n        fromView = toView;\n        advance();\n      }\n    }\n\n    document.getElementById(\"viewsBtn\").addEventListener(\"click\", ()=>{\n      tourOn = true;\n      userHoldUntil = performance.now() + 1200;\n      startLeg((tourIndex + 1) % views.length);\n    });\n\n    document.getElementById(\"exitBtn\").addEventListener(\"click\", ()=>{\n      tourOn = true;\n      userHoldUntil = performance.now() + 900;\n      startLeg(0);\n      focusTextWall();\n    });\n\n    const cycleEl = document.getElementById(\"cycleWord\");\n    const cycleWords = [\"hyperculture\", \"and\", \"art\"];\n    let cycleIndex = 0;\n    function cycleTick(){\n      cycleEl.textContent = cycleWords[cycleIndex];\n      cycleIndex = (cycleIndex + 1) % cycleWords.length;\n    }\n    cycleTick();\n    setInterval(cycleTick, 1200);\n\n    let tAccum = 0;\n\n    function tick(){\n      const now = performance.now();\n      updateTour(now);\n\n      rotX += (targetRotX - rotX) * 0.10;\n      rotY += (targetRotY - rotY) * 0.10;\n      lookTarget.lerp(targetLook, 0.08);\n\n      tAccum += 0.016;\n      const microYaw = 0.0042 * Math.sin(tAccum * 0.55);\n      const microPitch = 0.0030 * Math.sin(tAccum * 0.37 + 1.3);\n\n      const rx = rotX + microPitch;\n      const ry = rotY + microYaw;\n\n      const cx = Math.cos(ry) * Math.cos(rx) * distance;\n      const cy = Math.sin(rx) * distance;\n      const cz = Math.sin(ry) * Math.cos(rx) * distance;\n\n      camera.position.set(cx, cy + 2.2, cz);\n      if(camera.position.y < 1.0) camera.position.y = 1.0;\n      camera.lookAt(lookTarget);\n\n      renderer.render(scene, camera);\n      requestAnimationFrame(tick);\n    }\n    requestAnimationFrame(tick);\n\n    function activateTyping(){\n      textInput.focus();\n      textInput.setSelectionRange(textInput.value.length, textInput.value.length);\n    }\n    setTimeout(activateTyping, 80);\n\n    textInput.addEventListener(\"pointerdown\", ()=> setTimeout(activateTyping, 0));\n\n    focusTextWall();\n    showToast(\"TEXT WALL\");\n  })();\n  <\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>HYPERMUSEUM HYPERMUSEUM TEXT WALL VIEWS EXIT Enter your Text drag = look&nbsp;&nbsp; pinch\/wheel = zoom&nbsp;&nbsp; tap\/click = focus<\/p>\n","protected":false},"featured_media":0,"menu_order":0,"template":"","meta":[],"hall":[],"room_type":[],"class_list":["post-111","raum","type-raum","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/hypermuseum.org\/index.php?rest_route=\/wp\/v2\/raum\/111","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hypermuseum.org\/index.php?rest_route=\/wp\/v2\/raum"}],"about":[{"href":"https:\/\/hypermuseum.org\/index.php?rest_route=\/wp\/v2\/types\/raum"}],"wp:attachment":[{"href":"https:\/\/hypermuseum.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=111"}],"wp:term":[{"taxonomy":"hall","embeddable":true,"href":"https:\/\/hypermuseum.org\/index.php?rest_route=%2Fwp%2Fv2%2Fhall&post=111"},{"taxonomy":"room_type","embeddable":true,"href":"https:\/\/hypermuseum.org\/index.php?rest_route=%2Fwp%2Fv2%2Froom_type&post=111"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}