| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> |
| <title>Monuments de Paris — Splat Captures</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet"> |
| <style> |
| :root{ |
| --ink:#0b0a09; |
| --paper:#ece4d6; |
| --paper-dim:#9a9286; |
| --gold:#c9a96a; |
| --gold-bright:#e6c98a; |
| --line:rgba(201,169,106,.28); |
| } |
| *{margin:0;padding:0;box-sizing:border-box} |
| html,body{height:100%;overflow:hidden;background:var(--ink);color:var(--paper)} |
| body{ |
| font-family:"IBM Plex Mono",monospace; |
| -webkit-font-smoothing:antialiased; |
| position:fixed;inset:0; |
| touch-action:none; |
| } |
| /* atmospheric background */ |
| #bg{position:absolute;inset:0;z-index:0; |
| background: |
| radial-gradient(120% 90% at 50% 38%, rgba(80,64,38,.20), rgba(11,10,9,0) 55%), |
| radial-gradient(140% 120% at 50% 120%, rgba(201,169,106,.06), rgba(11,10,9,0) 50%), |
| #0b0a09; |
| } |
| /* the splat viewer injects its own canvas here */ |
| #viewer{position:absolute;inset:0;z-index:1} |
| #viewer canvas{display:block} |
| /* grain + vignette */ |
| #vignette{position:absolute;inset:0;z-index:2;pointer-events:none; |
| background:radial-gradient(120% 100% at 50% 45%, rgba(0,0,0,0) 45%, rgba(0,0,0,.6) 100%);} |
| #grain{position:absolute;inset:-50%;z-index:3;pointer-events:none;opacity:.045; |
| background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); |
| animation:grain 6s steps(6) infinite;} |
| @keyframes grain{0%{transform:translate(0,0)}50%{transform:translate(-3%,2%)}100%{transform:translate(2%,-2%)}} |
|
|
| .ui{position:absolute;z-index:4} |
| /* top bar */ |
| .topbar{top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start; |
| padding:22px 26px;pointer-events:none;} |
| .brand{display:flex;align-items:center;gap:10px;font-size:11px;letter-spacing:.28em;color:var(--paper-dim);text-transform:uppercase;} |
| .dot{width:6px;height:6px;border-radius:50%;background:var(--gold);box-shadow:0 0 10px var(--gold);animation:pulse 2.4s ease-in-out infinite} |
| @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}} |
| .topright{text-align:right;font-size:11px;letter-spacing:.18em;color:var(--paper-dim);pointer-events:auto} |
| .galnav{display:flex;gap:14px;font-size:11px;letter-spacing:.2em;text-transform:uppercase} |
| .galnav a{color:var(--paper-dim);text-decoration:none;transition:.25s} |
| .galnav a:hover{color:var(--gold-bright)} |
| .galnav a.gactive{color:var(--gold);border-bottom:1px solid var(--gold);padding-bottom:2px;pointer-events:none} |
| .brand-col{display:flex;flex-direction:column;gap:7px;pointer-events:none} |
| .agentlink{pointer-events:auto;width:max-content;font-size:10px;letter-spacing:.16em; |
| color:var(--gold);text-transform:uppercase;text-decoration:none; |
| border-bottom:1px solid var(--line);padding-bottom:2px;transition:.25s} |
| .agentlink:hover{color:var(--gold-bright);border-color:var(--gold)} |
| .toggle{cursor:pointer;border:1px solid var(--line);padding:7px 12px;border-radius:2px; |
| color:var(--paper);background:rgba(201,169,106,.04);transition:.25s;user-select:none} |
| .toggle:hover{border-color:var(--gold);color:var(--gold-bright)} |
| .toggle.off{opacity:.45} |
|
|
| /* title */ |
| .title{top:60px;left:0;right:0;text-align:center;pointer-events:none} |
| .title .kick{font-size:11px;letter-spacing:.5em;color:var(--gold);text-transform:uppercase;margin-bottom:6px} |
| .title h1{font-family:"Cormorant Garamond",serif;font-weight:500;font-size:clamp(34px,6vw,68px); |
| line-height:.95;letter-spacing:.01em} |
| .title h1 em{font-style:italic;color:var(--gold-bright)} |
| .rule{width:54px;height:1px;background:var(--gold);margin:14px auto 0;opacity:.6} |
|
|
| /* info panel */ |
| .info{left:26px;bottom:150px;max-width:340px;pointer-events:none; |
| opacity:0;transform:translateY(8px);transition:opacity .6s,transform .6s} |
| .info.show{opacity:1;transform:none} |
| .info .now{font-size:10px;letter-spacing:.3em;color:var(--paper-dim);text-transform:uppercase} |
| .info .name{font-family:"Cormorant Garamond",serif;font-weight:500;font-size:clamp(28px,5vw,46px); |
| line-height:1.02;margin:6px 0 4px} |
| .info .loc{font-size:11px;letter-spacing:.14em;color:var(--gold)} |
| .info .line{font-family:"Cormorant Garamond",serif;font-style:italic;font-size:18px;color:var(--paper-dim);margin-top:12px;line-height:1.3} |
| .info .stats{margin-top:14px;font-size:10.5px;letter-spacing:.1em;color:var(--paper-dim); |
| border-top:1px solid var(--line);padding-top:10px} |
| .info .stats b{color:var(--paper);font-weight:400} |
|
|
| /* card rail */ |
| .rail{left:0;right:0;bottom:0;display:flex;gap:10px;padding:18px 26px 22px; |
| overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch} |
| .rail::-webkit-scrollbar{display:none} |
| .card{flex:0 0 auto;min-width:128px;cursor:pointer;padding:12px 14px;border:1px solid rgba(255,255,255,.07); |
| border-radius:3px;background:rgba(255,255,255,.015);transition:.3s;backdrop-filter:blur(4px)} |
| .card:hover{border-color:var(--line);background:rgba(201,169,106,.05)} |
| .card.active{border-color:var(--gold);background:rgba(201,169,106,.09)} |
| .card .idx{font-size:10px;letter-spacing:.2em;color:var(--gold)} |
| .card .cn{font-family:"Cormorant Garamond",serif;font-size:20px;line-height:1.05;margin:4px 0 5px} |
| .card .meta{font-size:9.5px;letter-spacing:.08em;color:var(--paper-dim)} |
| .card.active .meta{color:var(--gold)} |
|
|
| /* hint */ |
| .hint{left:0;right:0;bottom:128px;text-align:center;font-size:10.5px;letter-spacing:.28em; |
| color:var(--paper-dim);text-transform:uppercase;pointer-events:none;transition:opacity .8s} |
| .hint.hide{opacity:0} |
|
|
| /* loader */ |
| #loader{position:absolute;inset:0;z-index:9;display:flex;align-items:center;justify-content:center; |
| background:var(--ink);transition:opacity .8s;flex-direction:column;gap:14px} |
| #loader.gone{opacity:0;pointer-events:none} |
| #loader .l{font-size:11px;letter-spacing:.4em;color:var(--gold);text-transform:uppercase} |
| #loader .bar{width:120px;height:1px;background:rgba(201,169,106,.2);overflow:hidden;position:relative} |
| #loader .bar i{display:block;height:100%;width:40%;background:var(--gold);position:absolute;left:0; |
| animation:slide 1.1s ease-in-out infinite} |
| #loader.determinate .bar i{animation:none;width:var(--p,0%);transition:width .3s} |
| @keyframes slide{0%{transform:translateX(-120%)}100%{transform:translateX(330%)}} |
| #loader .pct{font-size:10px;letter-spacing:.2em;color:var(--paper-dim)} |
|
|
| @media(max-width:720px){ |
| .title{top:54px} |
| .info{bottom:140px;left:18px;right:18px;max-width:none;text-align:center} |
| .info .stats{display:inline-block} |
| .info .line{display:none} |
| .topbar{padding:16px 18px} |
| .rail{padding:14px 18px 18px} |
| .hint{bottom:120px} |
| } |
| </style> |
| </head> |
| <body> |
| <div id="bg"></div> |
| <div id="viewer"></div> |
| <div id="vignette"></div> |
| <div id="grain"></div> |
|
|
| <div class="ui topbar"> |
| <div class="brand-col"> |
| <div class="brand"><span class="dot"></span>Gaussian Splat Capture</div> |
| <a class="agentlink" href="https://huggingface.co/spaces/mishig/monuments-de-paris/blob/main/README.md" target="_blank" rel="noopener">↗ Built with an agent — make your own</a> |
| </div> |
| <nav class="topright galnav"> |
| <a href="https://mishig-monuments-de-paris.static.hf.space" class="gactive">Paris</a> |
| <a href="https://mishig-monuments-of-japan.static.hf.space">Japon</a> |
| <a href="https://mishig-monuments-of-egypt.static.hf.space">Égypte</a> |
| </nav> |
| </div> |
|
|
| <div class="ui title"> |
| <div class="kick">Collection · Île-de-France</div> |
| <h1>Monuments de <em>Paris</em></h1> |
| <div class="rule"></div> |
| </div> |
|
|
| <div class="ui info" id="info"> |
| <div class="now">En vue</div> |
| <div class="name" id="i-name">—</div> |
| <div class="loc" id="i-loc">—</div> |
| <div class="line" id="i-line">—</div> |
| <div class="stats" id="i-stats">—</div> |
| </div> |
|
|
| <div class="ui hint" id="hint">Glissez pour pivoter · molette pour changer de monument</div> |
|
|
| <div class="ui rail" id="rail"></div> |
|
|
| <div id="loader"><div class="l">Reconstruction</div><div class="bar"><i></i></div><div class="pct" id="pct"></div></div> |
|
|
| <script type="importmap"> |
| { |
| "imports": { |
| "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js", |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/", |
| "@mkkellogg/gaussian-splats-3d": "https://cdn.jsdelivr.net/npm/@mkkellogg/gaussian-splats-3d@0.4.6/build/gaussian-splats-3d.module.js" |
| } |
| } |
| </script> |
| <script type="module"> |
| import * as THREE from 'three'; |
| import * as GS from '@mkkellogg/gaussian-splats-3d'; |
| |
| |
| |
| |
| |
| |
| |
| |
| const MON=[ |
| {file:"ksplat/eiffel-square.ksplat", name:"Tour Eiffel", loc:"Champ-de-Mars · 7ᵉ", |
| line:"La dame de fer sur le tapis vert du Champ-de-Mars.", stats:"524K splats · TripoSplat · 2026"}, |
| {file:"ksplat/opera-garnier.ksplat", name:"Opéra Garnier", loc:"Place de l'Opéra · 9ᵉ", |
| line:"Faste Second Empire sous le dôme de cuivre.", stats:"262K splats · TripoSplat · 2026"}, |
| {file:"ksplat/arc-de-triomphe.ksplat",name:"Arc de Triomphe", loc:"Place Charles-de-Gaulle · 8ᵉ", |
| line:"Pierre et mémoire au bout des Champs-Élysées.", stats:"262K splats · TripoSplat · 2026"}, |
| {file:"ksplat/sacre-coeur.ksplat", name:"Sacré-Cœur", loc:"Butte Montmartre · 18ᵉ", |
| line:"Travertin blanc veillant sur la ville.", stats:"262K splats · TripoSplat · 2026"}, |
| {file:"ksplat/moulin-rouge.ksplat", name:"Moulin Rouge", loc:"Boulevard de Clichy · 18ᵉ", |
| line:"Le moulin écarlate qui ne tourne que la nuit.", stats:"524K splats · TripoSplat · 2026"}, |
| {file:"ksplat/pantheon.ksplat", name:"Panthéon", loc:"Montagne Sainte-Geneviève · 5ᵉ", |
| line:"Aux grands hommes, la patrie reconnaissante.", stats:"262K splats · TripoSplat · 2026"}, |
| ]; |
| |
| const viewerEl = document.getElementById('viewer'); |
| const loader = document.getElementById('loader'); |
| const pctEl = document.getElementById('pct'); |
| const info = document.getElementById('info'); |
| const hint = document.getElementById('hint'); |
| |
| |
| const renderer = new THREE.WebGLRenderer({antialias:false, alpha:true}); |
| renderer.setPixelRatio(Math.min(devicePixelRatio,2)); |
| renderer.setClearColor(0x000000,0); |
| viewerEl.appendChild(renderer.domElement); |
| |
| const camera = new THREE.PerspectiveCamera(45, innerWidth/innerHeight, 0.1, 500); |
| |
| |
| let viewer = null; |
| let current = -1, switching = false; |
| |
| function makeViewer(){ |
| return new GS.Viewer({ |
| renderer, |
| camera, |
| useBuiltInControls:false, |
| selfDrivenMode:false, |
| sharedMemoryForWorkers:false, |
| gpuAcceleratedSort:false, |
| sphericalHarmonicsDegree:0, |
| antialiased:true, |
| ignoreDevicePixelRatio:false, |
| dynamicScene:false, |
| }); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let prevViewer=null; |
| let prevTarget=new THREE.Vector3(), prevRadius=3; |
| let tPhase='idle', tStart=0, navDir=1; |
| let slideA=0, slideB=0; |
| let slideDist=4, showCurrent=true; |
| const SLIDE_DUR=520; |
| const easeIO=t=>t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2; |
| |
| function slideSpan(rad){ |
| const fov=THREE.MathUtils.degToRad(camera.fov); |
| return rad*Math.tan(fov*0.5)*Math.max(1,camera.aspect) + rad*0.7; |
| } |
| |
| function updateTransition(now){ |
| if(tPhase!=='slide') return; |
| let p=(now-tStart)/SLIDE_DUR; if(p>=1)p=1; |
| const e=easeIO(p); |
| slideA = e * ( slideDist*navDir); |
| slideB = (1-e) * (-slideDist*navDir); |
| if(p>=1){ |
| tPhase='idle'; slideA=0; slideB=0; |
| if(prevViewer){ const pv=prevViewer; prevViewer=null; try{ pv.dispose(); }catch(e){} } |
| info.classList.add('show'); |
| } |
| } |
| |
| async function switchTo(i, dir){ |
| if(switching || i===current) return; |
| switching = true; |
| const prev = current; |
| current = i; |
| const m = MON[i]; |
| navDir = (dir!==undefined) ? dir : (prev<0 ? 1 : (i>prev ? 1 : -1)); |
| |
| |
| document.getElementById('i-name').textContent = m.name; |
| document.getElementById('i-loc').textContent = m.loc; |
| document.getElementById('i-line').textContent = '« '+m.line+' »'; |
| document.getElementById('i-stats').innerHTML = '<b>'+m.stats.split(' · ').join('</b> · <b>')+'</b>'; |
| info.classList.remove('show'); |
| [...rail.children].forEach((c,j)=>c.classList.toggle('active', j===i)); |
| centerActiveCard(i); |
| |
| |
| if(viewer && prev!==-1){ prevViewer = viewer; prevTarget.copy(target); prevRadius = radius; } |
| else { prevViewer = null; } |
| viewer = null; showCurrent = false; tPhase = 'idle'; slideA = 0; slideB = 0; |
| |
| |
| if(prev===-1){ loader.classList.remove('gone'); loader.classList.add('determinate'); setProgress(0); } |
| else { loader.classList.add('gone'); } |
| |
| |
| const v = makeViewer(); |
| try{ |
| await v.addSplatScene(m.file, { |
| showLoadingUI:false, |
| progressiveLoad:false, |
| splatAlphaRemovalThreshold:8, |
| rotation:[1,0,0,0], |
| onProgress:(pct)=>setProgress(pct), |
| }); |
| viewer = v; window.__viewer = viewer; window.__THREE = THREE; |
| frameObject(); |
| slideDist = Math.max(slideSpan(prevRadius), slideSpan(radius)); |
| slideB = -slideDist*navDir; |
| showCurrent = true; |
| if(prev===-1){ loader.classList.add('gone'); prefetchRest(); } |
| tStart = performance.now(); tPhase = 'slide'; |
| }catch(err){ |
| console.error('Failed to load', m.file, err); |
| loader.classList.remove('gone'); pctEl.textContent='échec du chargement'; |
| if(!viewer){ viewer = prevViewer; prevViewer = null; } |
| }finally{ |
| switching = false; |
| resetIdle(); |
| } |
| } |
| |
| function setProgress(pct){ |
| pct = Math.max(0, Math.min(100, Math.round(pct||0))); |
| loader.style.setProperty('--p', pct+'%'); |
| pctEl.textContent = pct+'%'; |
| } |
| |
| |
| let theta=0.6, phi=1.15, radius=4.2, target=new THREE.Vector3(0,0,0); |
| let lastInteract=0, autoRotate=true; |
| const pointers=new Map(); let lastPinch=0; |
| |
| |
| |
| |
| |
| const FRAME_MARGIN = 2.15; |
| const frameCache = {}; |
| |
| function sampleBounds(){ |
| const xs=[], ys=[], zs=[]; let sx=0, sz=0; |
| try{ |
| const mesh = viewer.splatMesh; |
| const count = mesh.getSplatCount(); |
| const step = Math.max(1, Math.floor(count/20000)); |
| const c = new THREE.Vector3(); |
| for(let i=0;i<count;i+=step){ |
| mesh.getSplatCenter(i, c, true); |
| xs.push(c.x); ys.push(c.y); zs.push(c.z); sx+=c.x; sz+=c.z; |
| } |
| }catch(e){} |
| if(xs.length>10){ |
| const n = xs.length; |
| const pct=(arr,p)=>{const a=arr.slice().sort((u,v)=>u-v); |
| return a[Math.min(a.length-1,Math.max(0,Math.round(p*(a.length-1))))];}; |
| |
| const p1x=pct(xs,.01),p99x=pct(xs,.99),p1z=pct(zs,.01),p99z=pct(zs,.99), |
| p1y=pct(ys,.005),p99y=pct(ys,.995); |
| frameCache[current] = { |
| |
| |
| cx: sx/n, |
| cz: sz/n, |
| cy: (p1y+p99y)/2, |
| h: p99y-p1y, |
| |
| wDiag: Math.hypot(p99x-p1x, p99z-p1z), |
| }; |
| } |
| } |
| |
| |
| function applyFraming(){ |
| const f = frameCache[current]; |
| if(!f){ target.set(0,0,0); radius=3; minR=1.2; maxR=8; return; } |
| target.set(f.cx, f.cy, f.cz); |
| const fov = THREE.MathUtils.degToRad(camera.fov); |
| const fitH = (f.h*0.5)/Math.tan(fov*0.5); |
| const fitW = (f.wDiag*0.5)/Math.tan(fov*0.5)/camera.aspect; |
| radius = Math.max(fitH, fitW) * FRAME_MARGIN + 0.05; |
| minR = radius*0.4; maxR = radius*2.6; |
| } |
| |
| function frameObject(){ sampleBounds(); applyFraming(); } |
| let minR=2, maxR=10; |
| |
| |
| const _right=new THREE.Vector3(); |
| function applyCamFor(tgt, rad, pan){ |
| const sp=Math.sin(phi); |
| camera.position.set( |
| tgt.x+rad*sp*Math.sin(theta), |
| tgt.y+rad*Math.cos(phi), |
| tgt.z+rad*sp*Math.cos(theta)); |
| camera.lookAt(tgt); |
| if(pan){ |
| |
| _right.set(1,0,0).applyQuaternion(camera.quaternion); |
| camera.position.addScaledVector(_right, pan); |
| camera.lookAt(tgt.x+_right.x*pan, tgt.y+_right.y*pan, tgt.z+_right.z*pan); |
| } |
| camera.updateMatrixWorld(); |
| } |
| |
| const dom = renderer.domElement; |
| dom.addEventListener('pointerdown',e=>{pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});resetIdle();hideHint();}); |
| dom.addEventListener('pointermove',e=>{ |
| if(!pointers.has(e.pointerId))return;const p=pointers.get(e.pointerId); |
| |
| theta-=(e.clientX-p.x)*0.006; phi-=(e.clientY-p.y)*0.006; |
| phi=Math.max(0.18,Math.min(Math.PI-0.18,phi)); |
| pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});resetIdle(); |
| }); |
| function endPtr(e){pointers.delete(e.pointerId);resetIdle();} |
| dom.addEventListener('pointerup',endPtr);dom.addEventListener('pointercancel',endPtr); |
| function resetIdle(){lastInteract=performance.now();} |
| |
| |
| function navigate(dir){ |
| if(switching) return; |
| const n=(current+dir+MON.length)%MON.length; |
| if(n!==current) switchTo(n, dir); |
| } |
| let wheelAccum=0; |
| dom.addEventListener('wheel',e=>{ |
| e.preventDefault(); hideHint(); resetIdle(); |
| if(switching){ wheelAccum=0; return; } |
| wheelAccum += e.deltaY; |
| if(Math.abs(wheelAccum) >= 120){ navigate(wheelAccum>0?1:-1); wheelAccum=0; } |
| },{passive:false}); |
| addEventListener('keydown',e=>{ |
| if(['ArrowDown','ArrowRight','PageDown',' '].includes(e.key)){ e.preventDefault(); navigate(1); hideHint(); } |
| else if(['ArrowUp','ArrowLeft','PageUp'].includes(e.key)){ e.preventDefault(); navigate(-1); hideHint(); } |
| }); |
| |
| let hinted=false; |
| function hideHint(){if(!hinted){hinted=true;hint.classList.add('hide');}} |
| |
| |
| |
| |
| |
| |
| let prefetched=false; |
| function prefetchRest(){ |
| if(prefetched) return; prefetched=true; |
| const others = MON.map(m=>m.file).filter((_,j)=>j!==current); |
| let k=0; |
| (function next(){ |
| if(k>=others.length) return; |
| fetch(others[k++], {cache:'force-cache'}).catch(()=>{}).finally(()=>setTimeout(next, 250)); |
| })(); |
| } |
| |
| const rail=document.getElementById('rail'); |
| MON.forEach((m,i)=>{const d=document.createElement('div');d.className='card'; |
| d.innerHTML='<div class="idx">'+String(i+1).padStart(2,'0')+'</div><div class="cn">'+m.name+'</div><div class="meta">'+m.loc+'</div>'; |
| d.addEventListener('click',()=>switchTo(i));rail.appendChild(d);}); |
| |
| |
| |
| function centerActiveCard(i){ |
| const c=rail.children[i]; if(!c) return; |
| const left = c.offsetLeft - (rail.clientWidth - c.clientWidth)/2; |
| const max = rail.scrollWidth - rail.clientWidth; |
| rail.scrollTo({left: Math.max(0, Math.min(left, max)), behavior:'smooth'}); |
| } |
| |
| |
| function resize(){ |
| const w=innerWidth,h=innerHeight; |
| renderer.setPixelRatio(Math.min(devicePixelRatio,2)); |
| renderer.setSize(w,h); |
| camera.aspect=w/h; camera.updateProjectionMatrix(); |
| applyFraming(); |
| } |
| addEventListener('resize',resize); |
| addEventListener('orientationchange',resize); |
| resize(); |
| |
| |
| renderer.autoClear = false; |
| function loop(now){ |
| requestAnimationFrame(loop); |
| |
| if(autoRotate && pointers.size===0) theta+=0.0016; |
| updateTransition(now); |
| renderer.clear(); |
| |
| if(prevViewer){ |
| applyCamFor(prevTarget, prevRadius, slideA); |
| try{ prevViewer.update(); prevViewer.render(); }catch(e){} |
| } |
| |
| if(viewer && showCurrent){ |
| applyCamFor(target, radius, slideB); |
| try{ viewer.update(); viewer.render(); }catch(e){} |
| } |
| } |
| requestAnimationFrame(loop); |
| window.__phase=()=>tPhase; |
| window.__lock=()=>{}; window.__unlock=()=>{}; |
| window.__idx=()=>current; window.__radius=()=>+radius.toFixed(3); window.__nav=(d)=>navigate(d); |
| window.__theta=()=>+theta.toFixed(4); |
| window.__slide=()=>[+slideA.toFixed(3),+slideB.toFixed(3)]; |
| window.__dbg=()=>({ph:tPhase, prev:!!prevViewer, show:showCurrent, cur:!!viewer}); |
| window.__setView=(t,ph)=>{autoRotate=false;theta=t;if(ph!==undefined)phi=ph;}; |
| window.__target=()=>[+target.x.toFixed(3),+target.y.toFixed(3),+target.z.toFixed(3)]; |
| |
| |
| switchTo(0); |
| </script> |
| </body> |
| </html> |
|
|