import './style.css'; import { defaults, experiments, format, type Experiment, type Parameters } from './experiments'; import { createLab } from './scene/lab'; const app = document.querySelector('#app')!; app.innerHTML = `
E/LAB

Electrotechnique & Electrophysics

Explore the invisible. Understand the physical.

INTERACTIVE LAB
3D VIEW

THE PHYSICS

TRY THIS

Model & visual scales

`; const lab = createLab(document.querySelector('#viewport')!); const settings = new Map(experiments.map(experiment => [experiment.id, defaults(experiment)])); const byId = (id: string) => document.getElementById(id)!; let active = experiments[0]; let paused = window.matchMedia('(prefers-reduced-motion: reduce)').matches; function updatePause() { byId('pause').textContent = paused ? 'Resume animation' : 'Pause animation'; byId('pause').setAttribute('aria-pressed', String(paused)); lab.setPaused(paused); } updatePause(); function parameters(): Parameters { return settings.get(active.id)!; } function update() { const p = parameters(); for (const control of active.controls) { if (control.kind !== 'range') continue; byId(`output-${control.id}`).textContent = `${format(Number(p[control.id]), 3)}${control.unit ? ` ${control.unit}` : ''}`; } if (active.id === 'semiconductor') { (byId('parameter-concentration') as HTMLInputElement).disabled = p.doping === 'intrinsic'; } byId('measurements').innerHTML = active.measure(p).map(m => `
${m.label}
${m.value}${m.unit}
`).join(''); lab.update(active.id, p); } function show(experiment: Experiment) { active = experiment; const index = experiments.indexOf(experiment); byId('lesson-number').textContent = `EXPERIMENT ${String(index + 1).padStart(2, '0')} / 08`; byId('lesson-title').textContent = experiment.title; byId('description').textContent = experiment.description; byId('scene-note').textContent = experiment.sceneNote; byId('explanation').textContent = experiment.explanation; byId('challenge').textContent = experiment.challenge; byId('limits').textContent = experiment.limits; byId('equations').replaceChildren(...experiment.equations.map(equation => { const div = document.createElement('p'); div.textContent = equation; return div; })); const source = byId('source') as HTMLAnchorElement; source.textContent = `${experiment.source.title} ↗`; source.href = experiment.source.url; byId('legend').innerHTML = experiment.legend.map(([colour, text]) => `${text}`).join(''); byId('lesson').setAttribute('aria-labelledby', `tab-${experiment.id}`); document.querySelectorAll('[role="tab"]').forEach(tab => { const selected = tab.dataset.experiment === experiment.id; tab.setAttribute('aria-selected', String(selected)); tab.tabIndex = selected ? 0 : -1; }); const p = parameters(); byId('parameters').innerHTML = experiment.controls.map(control => { const id = `parameter-${control.id}`; if (control.kind === 'select') return `
`; return `
`; }).join(''); lab.resetCamera(); update(); } function fromHash() { const id = window.location.hash.slice(1); show(experiments.find(experiment => experiment.id === id) ?? experiments[0]); } const onHashChange = () => fromHash(); window.addEventListener('hashchange', onHashChange); fromHash(); byId('parameters').addEventListener('input', event => { const input = event.target as HTMLInputElement | HTMLSelectElement; if (!input.name) return; parameters()[input.name] = input.type === 'range' ? Number(input.value) : input.value; update(); }); byId('reset').addEventListener('click', () => { settings.set(active.id, defaults(active)); show(active); }); byId('camera-reset').addEventListener('click', () => lab.resetCamera()); byId('pause').addEventListener('click', () => { paused = !paused; updatePause(); }); const tabs = [...document.querySelectorAll('[role="tab"]')]; tabs.forEach((tab, index) => { tab.addEventListener('click', () => { const id = tab.dataset.experiment!; if (window.location.hash === `#${id}`) show(experiments[index]); else window.location.hash = id; }); tab.addEventListener('keydown', event => { let next: number; if (event.key === 'ArrowRight') next = (index + 1) % tabs.length; else if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length; else if (event.key === 'Home') next = 0; else if (event.key === 'End') next = tabs.length - 1; else return; event.preventDefault(); tabs[next].focus(); tabs[next].click(); }); }); if (import.meta.hot) import.meta.hot.dispose(() => { window.removeEventListener('hashchange', onHashChange); lab.dispose(); });