From 4533910bce394e7a0ee6debfc05ca1221ee51899 Mon Sep 17 00:00:00 2001 From: victor Date: Sat, 24 Oct 2026 15:57:01 +0200 Subject: Astra! --- src/main.ts | 308 ++++++------------------------------------------------------ 1 file changed, 27 insertions(+), 281 deletions(-) (limited to 'src/main.ts') diff --git a/src/main.ts b/src/main.ts index e419e46..8d8c5d5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,288 +1,34 @@ import './style.css'; -import { defaults, experiments, formatDisplay, type Experiment, type Parameters } from './experiments'; -import { getLanguage, languageOptions, setLanguage, t, type Chrome, type Language } from './i18n'; -import { presentLesson } from './i18n/present'; -import { createLab } from './scene/lab'; -import { answerUrl, getTestLesson, testLessons } from './test4'; -import { renderEquation, renderNotation, renderProse } from './math'; +import 'katex/dist/katex.min.css'; +import { createI18n } from './i18n'; +import { models } from './models/registry'; +import { icon } from './ui/icons'; + +const i18n = createI18n(); +const { t } = i18n; +document.documentElement.lang = i18n.locale; +document.title = t('app.title'); const app = document.querySelector('#app')!; app.innerHTML = ` -
- E/LAB -

Electrotechnique & Electrophysics

Explore the invisible. Understand the physical.

- + + -

FROM EQUATIONS TO INTUITION

Electric energy, made visible.

Explore the six questions from Test 4. Change the scene, make a prediction, then follow the derivation.

4A 100p4B 100p6 questions · 3D study guide
- -
- -
-
-
3D VIEW
-
- -
-
NOTICE WHAT CHANGES

-
-

THE PHYSICS

-

TRY THIS

Model & visual scales

-
-
-

BUILD YOUR ANSWER

Follow the reasoning

Original answers ↗
-
-

PAUSE & PREDICT

- -
-
-
`; - -const lab = createLab(document.querySelector('#viewport')!); -const allLessons: Experiment[] = [...testLessons, ...experiments]; -const settings = new Map(allLessons.map(experiment => [experiment.id, defaults(experiment)])); -const byId = (id: string) => document.getElementById(id)!; -function applyLanguage() { - document.documentElement.lang = getLanguage(); - document.title = t('documentTitle'); - document.querySelector('meta[name="description"]')?.setAttribute('content', t('metaDescription')); - (byId('language') as HTMLSelectElement).value = getLanguage(); - byId('language').setAttribute('aria-label', t('language')); - byId('language-label').textContent = t('language'); - type CopyKey = { [K in keyof Chrome]: Chrome[K] extends string ? K : never }[keyof Chrome]; - const set = (id: string, key: CopyKey) => { byId(id).textContent = t(key); }; - set('test-collection', 'test'); set('lab-collection', 'experiments'); - set('reset', 'reset'); set('camera-reset', 'camera'); - set('app-title', 'title'); - byId('pause').textContent = paused ? t('resume') : t('pause'); - byId('reveal-prediction').textContent = byId('prediction-answer').hidden ? t('reveal') : t('hide'); - document.querySelector('.brand')!.setAttribute('aria-label', t('home')); - document.querySelector('.header-title p')!.textContent = t('subtitle'); - document.querySelector('.collections')!.setAttribute('aria-label', t('collection')); - document.querySelector('.test-overview .eyebrow')!.textContent = t('overview'); - document.querySelector('.test-overview h2')!.textContent = t('overviewTitle'); - document.querySelector('.test-overview > div > p:not(.eyebrow)')!.textContent = t('overviewText'); - document.querySelector('.test-score > span')!.textContent = t('score'); - document.querySelector('.subjects')!.setAttribute('aria-label', t('subjects')); - document.querySelector('[role="tablist"]')!.setAttribute('aria-label', t('lessons')); - byId('test-question').setAttribute('aria-label', t('original')); - byId('parameters').setAttribute('aria-label', t('parameters')); - byId('measurements').setAttribute('aria-label', t('measurements')); - byId('viewport').setAttribute('aria-label', t('viewport')); - byId('test-study').setAttribute('aria-label', t('study')); - document.querySelector('.scene-toolbar .view-tag')!.textContent = t('view'); - document.querySelector('.scene-footer .hint')!.textContent = t('hint'); - document.querySelector('#observation-panel .eyebrow')!.textContent = t('notice'); - byId('observation-panel').setAttribute('aria-label', t('connect')); - document.querySelector('.learning-panel > div:first-child .eyebrow')!.textContent = t('physics'); - byId('learning-panel').setAttribute('aria-label', t('learn')); - document.querySelector('.challenge .eyebrow')!.textContent = t('try'); - document.querySelector('.challenge summary')!.textContent = t('scales'); - document.querySelector('.solution-heading .eyebrow')!.textContent = t('build'); - document.querySelector('.solution-heading h3')!.textContent = t('reasoning'); - document.querySelector('.solution-heading a')!.textContent = t('originalAnswers'); - document.querySelector('.prediction .eyebrow')!.textContent = t('predict'); - document.querySelector('.lesson-pagination')!.setAttribute('aria-label', t('navigation')); -} -let active: Experiment = testLessons[0]; -let collection = ''; -const subjects = document.querySelector('[role="tablist"]')!; -let paused = window.matchMedia('(prefers-reduced-motion: reduce)').matches; -function updatePause() { - byId('pause').textContent = paused ? t('resume') : t('pause'); - byId('pause').setAttribute('aria-pressed', String(paused)); - lab.setPaused(paused); -} -updatePause(); +
+
${icon('orbit')}${t('app.footer')}${t('app.footerNote')}
`; -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 = `${formatDisplay(Number(p[control.id]), 3)}${control.unit ? ` ${control.unit}` : ''}`; - } - if (active.id === 'semiconductor') { - (byId('parameter-concentration') as HTMLInputElement).disabled = p.doping === 'intrinsic'; - } - if (active.id === 'test-central') (byId('parameter-rb') as HTMLInputElement).disabled = p.destination === 'infinity'; - const testLesson = getTestLesson(active); - if (testLesson) renderProse(byId('observation'), presentLesson(testLesson).observe(p)); - byId('measurements').innerHTML = presentLesson(active).measure(p).map(m => `
${m.label}
${m.value}${m.unit}
`).join(''); - byId('measurements').querySelectorAll('.measurement > span').forEach(label => renderNotation(label, label.textContent!)); - lab.update(active.id, p); -} -let renderedLanguage = ''; -function show(experiment: Experiment) { - active = experiment; - const view = presentLesson(experiment); - const testLesson = getTestLesson(experiment); - const testView = testLesson ? presentLesson(testLesson) : undefined; - const lessons: Experiment[] = testLesson ? testLessons : experiments; - const index = lessons.indexOf(experiment); - const nextCollection = testLesson ? 'test' : 'lab'; - if (collection !== nextCollection || renderedLanguage !== getLanguage()) { - collection = nextCollection; - renderedLanguage = getLanguage(); - subjects.innerHTML = lessons.map((e, i) => ``).join(''); - } - for (const [id, selected] of [['test-collection', Boolean(testLesson)], ['lab-collection', !testLesson]] as const) { - if (selected) byId(id).setAttribute('aria-current', 'page'); - else byId(id).removeAttribute('aria-current'); - } - byId('test-overview').hidden = !testLesson; - byId('test-question').hidden = !testLesson; - byId('test-study').hidden = !testLesson; - byId('observation-panel').hidden = !testLesson; - byId('lesson-number').textContent = testView ? t('questionKicker')(testView.section, index + 1, testView.points) : t('experimentKicker')(index + 1, lessons.length); - if (testView) { - byId('test-question').replaceChildren(...testView.questions.map((text, i) => { - const paragraph = document.createElement('p'); - renderProse(paragraph, text); - if (i === 0) paragraph.className = 'question-lead'; - return paragraph; - })); - byId('solution').replaceChildren(...testView.answers.map(part => { - const details = document.createElement('details'); - const summary = document.createElement('summary'); - renderNotation(summary, part.title); - const steps = document.createElement('ol'); - steps.className = 'derivation'; - for (const step of part.steps) { - const item = document.createElement('li'); - const why = document.createElement('p'); renderProse(why, step.why); item.append(why); - if (step.math) { const math = document.createElement('p'); math.className = 'math-step'; renderEquation(math, step.math); item.append(math); } - steps.append(item); - } - details.append(summary, steps); - return details; - })); - renderProse(byId('prediction-question'), testView.prediction); - renderProse(byId('prediction-answer'), testView.predictionAnswer); - byId('prediction-answer').hidden = true; - byId('reveal-prediction').textContent = t('reveal'); - byId('reveal-prediction').setAttribute('aria-expanded', 'false'); - const previous = byId('previous-question') as HTMLAnchorElement; - const next = byId('next-question') as HTMLAnchorElement; - previous.hidden = index === 0; - next.hidden = index === testLessons.length - 1; - previous.textContent = t('previous'); - next.textContent = t('next'); - if (index > 0) previous.href = `#${testLessons[index - 1].id}`; - if (index < testLessons.length - 1) next.href = `#${testLessons[index + 1].id}`; - byId('question-progress').textContent = `${index + 1} / 6`; - } - byId('lesson-title').textContent = view.title; - renderProse(byId('description'), view.description); - renderProse(byId('scene-note'), view.sceneNote); - renderProse(byId('explanation'), view.explanation); - renderProse(byId('challenge'), view.challenge); - renderProse(byId('limits'), view.limits); - byId('equations').replaceChildren(...view.equations.map(equation => { - const div = document.createElement('p'); - renderEquation(div, equation); - return div; - })); - const source = byId('source') as HTMLAnchorElement; - source.textContent = `${view.source.title} ↗`; - source.href = view.source.url; - byId('legend').innerHTML = view.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 = view.controls.map(control => { - const id = `parameter-${control.id}`; - if (control.kind === 'select') return `
`; - return `
`; - }).join(''); - byId('parameters').querySelectorAll('label').forEach(label => { - const input = document.getElementById(label.htmlFor)!; - input.setAttribute('aria-label', label.textContent!); - renderNotation(label, label.textContent!); - }); - lab.resetCamera(experiment.id); - update(); -} -function fromHash() { - const id = window.location.hash.slice(1); - show(allLessons.find(experiment => experiment.id === id) ?? testLessons[0]); +const host = app.querySelector('main')!; +try { + const instance = await models[0].mount(host, { i18n }); + if (import.meta.hot) import.meta.hot.dispose(() => instance.dispose()); +} catch (error) { + console.error(error); + host.innerHTML = ``; + host.querySelector('button')?.addEventListener('click', () => window.location.reload()); } -let focusNextQuestion = false; -document.querySelector('.lesson-pagination')!.addEventListener('click', event => { - const link = (event.target as HTMLElement).closest('a'); - if (link && !((event as MouseEvent).ctrlKey || (event as MouseEvent).metaKey || (event as MouseEvent).shiftKey || (event as MouseEvent).altKey)) focusNextQuestion = true; -}); -const onHashChange = () => { - fromHash(); - if (focusNextQuestion) { - focusNextQuestion = false; - byId('lesson-title').focus({ preventScroll: true }); - byId('lesson-title').scrollIntoView({ block: 'start' }); - } -}; -window.addEventListener('hashchange', onHashChange); -fromHash(); -applyLanguage(); -byId('language').addEventListener('change', event => { - const next = (event.target as HTMLSelectElement).value; - if (next !== 'en' && next !== 'da') return; - setLanguage(next as Language); - applyLanguage(); - show(active); -}); -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(); -}); -byId('reveal-prediction').addEventListener('click', () => { - const reveal = byId('prediction-answer').hidden; - byId('prediction-answer').hidden = !reveal; - byId('reveal-prediction').setAttribute('aria-expanded', String(reveal)); - byId('reveal-prediction').textContent = reveal ? t('hide') : t('reveal'); -}); -subjects.addEventListener('click', event => { - const tab = (event.target as HTMLElement).closest('[data-experiment]'); - if (!tab) return; - const id = tab.dataset.experiment!; - if (window.location.hash === `#${id}`) show(allLessons.find(e => e.id === id)!); - else window.location.hash = id; -}); -subjects.addEventListener('keydown', event => { - const tabs = [...subjects.querySelectorAll('[role="tab"]')]; - const index = tabs.indexOf(event.target as HTMLButtonElement); - if (index < 0) return; - 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