diff --git a/index.html b/index.html index 64eb01a..eb773b3 100644 --- a/index.html +++ b/index.html @@ -17,9 +17,16 @@
+
+ + +
+

2D View

- +
+ +
diff --git a/src/main.js b/src/main.js index 4409d21..1ccc6ec 100644 --- a/src/main.js +++ b/src/main.js @@ -21,6 +21,7 @@ const canvas = document.getElementById('sim-canvas'); const view3dPanelEl = document.getElementById('view-3d-panel'); const view3dEl = document.getElementById('sim-3d'); const viewsWrapEl = document.querySelector('.views-wrap'); +const mobileToggleEl = document.getElementById('mobile-view-toggle'); const diagEl = document.getElementById('diag'); const state = { @@ -33,6 +34,7 @@ const state = { showAnnotations: false, show3d: true, canShow3d: true, + mobileView: '3d', // which view is active on mobile: '3d' | '2d' lengthCm: { ...DEFAULT_LENGTH_CM }, fallbackCount: 0, pose: null, @@ -56,22 +58,48 @@ const renderer3d = create3DRenderer(view3dEl); state.canShow3d = renderer3d.available; state.show3d = renderer3d.available; +/* ── Mobile helpers ── */ +function isMobileViewport() { + return window.innerWidth <= 768; +} + +function setMobileView(mode) { + state.mobileView = mode; // '3d' | '2d' + if (viewsWrapEl) viewsWrapEl.setAttribute('data-mobile-view', mode); + if (mobileToggleEl) { + for (const btn of mobileToggleEl.querySelectorAll('.toggle-btn')) { + btn.classList.toggle('active', btn.dataset.view === mode); + } + } + // On mobile, CSS handles panel display via data-mobile-view. + // Clear any inline display overrides so CSS can work. + if (view3dPanelEl) view3dPanelEl.style.display = ''; + // Let the 3D renderer know if it should actually render + const render3d = mode === '3d' && state.canShow3d; + state.show3d = render3d; + renderer3d.setVisible(render3d); + resizeViews(); +} + function resize2DCanvasToDisplaySize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); - const width = Math.max(1, Math.floor(canvas.clientWidth * dpr)); - const height = Math.max(1, Math.floor(canvas.clientHeight * dpr)); + const container = canvas.parentElement; + const width = Math.max(1, Math.floor(container.clientWidth * dpr)); + const height = Math.max(1, Math.floor(container.clientHeight * dpr)); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } } +/** Desktop-only: controls 3D panel visibility via inline styles */ function apply3DVisibility() { + if (isMobileViewport()) return; // mobile handled by setMobileView if (view3dPanelEl) { view3dPanelEl.style.display = state.show3d ? '' : 'none'; } if (viewsWrapEl) { - viewsWrapEl.style.gridTemplateColumns = state.show3d ? '1fr 1fr' : '1fr'; + viewsWrapEl.classList.toggle('single-view', !state.show3d); } renderer3d.setVisible(state.show3d); } @@ -295,13 +323,46 @@ const ui = wireUI(state, { }); state.pose = applyGrounding(solveWalker(state.theta, null, currentLengthScale())); -apply3DVisibility(); + +/* ── Mobile toggle button wiring ── */ +if (mobileToggleEl) { + mobileToggleEl.addEventListener('click', (e) => { + const btn = e.target.closest('.toggle-btn'); + if (!btn) return; + const view = btn.dataset.view; + if (view === '3d' || view === '2d') setMobileView(view); + }); +} + +// Initialize: on mobile start with 3D, on desktop show both +if (isMobileViewport()) { + if (state.canShow3d) { + setMobileView('3d'); + } else { + // WebGL unavailable – hide the toggle, force 2D + if (mobileToggleEl) mobileToggleEl.style.display = 'none'; + setMobileView('2d'); + } +} else { + apply3DVisibility(); +} resizeViews(); renderScene(canvas, state); if (state.show3d) renderer3d.render(state); updateDiagnostics(diagEl, state); -window.addEventListener('resize', resizeViews); +window.addEventListener('resize', () => { + if (isMobileViewport()) { + // Reapply current mobile view mode + setMobileView(state.mobileView); + } else { + // Desktop: show both panels, remove mobile data attr + if (viewsWrapEl) viewsWrapEl.removeAttribute('data-mobile-view'); + state.show3d = state.canShow3d; + apply3DVisibility(); + } + resizeViews(); +}); const fixedDt = 1 / 240; let acc = 0; diff --git a/styles.css b/styles.css index 2445721..46152e4 100644 --- a/styles.css +++ b/styles.css @@ -11,17 +11,30 @@ box-sizing: border-box; } +html, +body { + overflow-x: hidden; + max-width: 100vw; +} + body { margin: 0; font-family: Inter, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--text); + line-height: 1.4; } .app { display: grid; grid-template-columns: 1fr 320px; min-height: 100vh; + max-width: 100vw; +} + +/* ── Mobile view toggle (hidden on desktop) ── */ +.mobile-view-toggle { + display: none; } .views-wrap { @@ -32,6 +45,10 @@ body { min-height: 0; } +.views-wrap.single-view { + grid-template-columns: 1fr; +} + .view-panel { min-height: 0; display: flex; @@ -42,7 +59,24 @@ body { margin: 0 0 8px; } -#sim-canvas, +.canvas-container { + width: 100%; + height: 100%; + min-height: 260px; + background: #0f1116; + border: 1px solid #2a3342; + border-radius: 8px; + overflow: hidden; + position: relative; + flex: 1 1 0%; +} + +#sim-canvas { + width: 100%; + height: 100%; + display: block; +} + .sim-3d { width: 100%; height: 100%; @@ -50,16 +84,15 @@ body { border: 1px solid #2a3342; border-radius: 8px; min-height: 260px; -} - -.sim-3d { overflow: hidden; + flex: 1 1 0%; } .panel { background: var(--panel); border-left: 1px solid #2a3342; padding: 16px; + min-width: 0; } #tweakpane-root { @@ -158,6 +191,119 @@ select { .views-wrap { grid-template-columns: 1fr; - min-height: 70vh; + min-height: auto; + } + + .canvas-container, + .sim-3d { + aspect-ratio: 4 / 3; + height: auto; + min-height: 220px; + } +} + +@media (max-width: 768px) { + .mobile-view-toggle { + display: flex; + gap: 0; + border-radius: 8px; + overflow: hidden; + border: 1px solid #3d5274; + margin-bottom: 8px; + } + + .toggle-btn { + flex: 1; + padding: 10px 0; + border: none; + border-radius: 0; + background: #1d222b; + color: var(--muted); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.15s; + min-height: 0; + } + + .toggle-btn.active { + background: var(--accent); + color: #111; + } + + /* Hide the inactive view panel on mobile */ + .views-wrap[data-mobile-view="3d"] #view-2d-panel { + display: none; + } + + .views-wrap[data-mobile-view="2d"] #view-3d-panel { + display: none; + } + + /* Hide h2 titles on mobile (toggle replaces them) */ + .view-panel h2 { + display: none; + } + + .views-wrap { + padding: 8px; + gap: 8px; + } + + .canvas-container, + .sim-3d { + aspect-ratio: 4 / 3; + min-height: 200px; + border-radius: 6px; + } + + .panel { + padding: 12px; + } + + h1 { + font-size: 1rem; + margin-bottom: 10px; + } + + h2 { + margin-top: 10px; + font-size: 0.9rem; + } + + .row { + margin-bottom: 10px; + } + + button, + input[type='text'], + select { + min-height: 40px; + } +} + +@media (max-width: 480px) { + .views-wrap { + padding: 6px; + gap: 6px; + } + + .canvas-container, + .sim-3d { + aspect-ratio: 4 / 3; + min-height: 160px; + } + + .panel { + padding: 10px; + } + + .buttons { + grid-template-columns: 1fr; + } + + .diag pre { + font-size: 11px; + padding: 8px; } }