From c4c1c840f8761ea45a3d2dcc912a59d8ced78cb5 Mon Sep 17 00:00:00 2001 From: LalleSX Date: Sat, 24 Oct 2026 04:35:56 +0200 Subject: init --- src/main.ts | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/main.ts (limited to 'src/main.ts') diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..172f316 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,137 @@ +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(); +}); -- cgit v1.2.3