Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 22 additions & 45 deletions widgets/astrolabe-simulator/LESSON_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,51 +50,28 @@ value on the alidade, and teaches how to use the reading from that point on.

## Course sequence

### 1. Understand and configure the astrolabe front

**Prerequisites:** none.

This substantial lesson identifies the fixed plate, moving rete, and reading
rule; demonstrates their independent motion; and shows why the observer's
latitude determines the plate. It includes choosing London's exact plate,
observing the warning from a nearby plate, and restoring the exact match.

### 2. Set the astrolabe for a date and time

**Prerequisite:** lesson 1.

This substantial lesson carries July 14 from the back calendar to the
ecliptic-longitude scale, finds the same longitude on the front, sets the rule
to local apparent solar time, and turns the rete until the date point lies
beneath the rule. The result is a complete sky setting rather than an
unexplained rotation angle.

### 3. Read a star and follow its daily path

**Prerequisites:** lessons 1 and 2.

This substantial lesson locates Sirius on the rete, reads its altitude and
azimuth from the fixed plate, and then keeps the date point beneath the moving
rule while following Sirius to the eastern horizon, culmination, and western
horizon.

### 4. Solar operations

Keep these as separate future lessons:

- determine sunrise, noon, and sunset;
- read unequal hours on the front.

### 5. Back operations

Keep these as separate lessons:

- measure altitude with the alidade;
- use the equation-of-time loop;
- measure height with the shadow square;
- read a temporal hour on the double horary quadrant (implemented).

There is no section 6 in the current course plan.
The guide presents one ordered list rather than dividing operations by face.
Many real operations move between the front and back, so a face-based taxonomy
would be misleading.

1. **Understand and configure the astrolabe front.** Identify the fixed plate,
moving rete, and rule; compare a nearby plate with London's exact plate.
2. **Set the astrolabe for a date and time.** Carry July 14 from the back
calendar to ecliptic longitude, set the rule, and orient the rete.
3. **Read a star and follow its daily path.** Read Sirius and follow it through
rising, culmination, and setting.
4. **Find sunrise, noon, and sunset.** Carry the Sun's engraved date point
across the eastern horizon, meridian, and western horizon.
5. **Read a daylight temporal hour.** Use the point opposite the Sun and the
lower unequal-hour curves.
6. **Transfer a measured altitude to the alidade.** Explain the external
physical sighting and set its supplied angle on the simulated back.
7. **Convert apparent solar time to mean solar time.** Read and apply the
equation-of-time loop with its engraved sign convention.
8. **Measure a height with the shadow square.** Use an external angle and known
distance to read a proportional height.
9. **Read a temporal hour on the double horary quadrant.** Transfer the
curve-VI distance between noon and observed-altitude settings.

## Assessment and test requirements

Expand Down
12 changes: 12 additions & 0 deletions widgets/astrolabe-simulator/journey.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"steps": [
{
"eval": "(() => { const eclipticTicks = document.querySelectorAll('.astro-ecliptic-tick'); const limbTicks = document.querySelectorAll('.astro-limb-tick'); if (eclipticTicks.length !== 720 || !document.querySelector('[data-ecliptic-longitude=\"0.5\"]')) throw new Error(`front ecliptic is not graduated by half degrees (${eclipticTicks.length} ticks)`); if (document.querySelector('.astro-zodiac-label')) throw new Error('zodiac abbreviations remain on the front'); if (limbTicks.length !== 360 || !document.querySelector('[data-limb-degree=\"1\"]')) throw new Error(`front limb is not graduated by degrees (${limbTicks.length} ticks)`); const texture = document.querySelector('.astro-rete-edge-texture'); const filter = document.querySelector('#rete-edge-texture'); const textureStyle = texture && getComputedStyle(texture); if (!document.querySelector('.astro-rete-outer-rim') || !texture || !filter?.querySelector('feTurbulence') || textureStyle?.filter === 'none' || parseFloat(textureStyle?.strokeWidth || '0') < 18 || parseFloat(textureStyle?.opacity || '0') < 0.5) throw new Error('visible procedural rotating rete texture is missing'); const assists = document.querySelector('[data-testid=layer-artificialAssists]'); if (!assists || assists.checked || document.querySelector('.astro-sun')) throw new Error('artificial Sun assist is not off by default'); })()"
},
{
"eval": "(() => { const cards = [...document.querySelectorAll('.lesson-card')]; const starts = [...document.querySelectorAll('[data-testid^=\"start-\"]')]; if (cards.length !== 9 || starts.length !== 9) throw new Error(`complete curriculum not rendered (${cards.length} cards, ${starts.length} starts)`); if (document.querySelector('.future-card, .lesson-category, .lesson-card button:disabled')) throw new Error('lesson categories or placeholders remain'); const titles = cards.map((card) => card.querySelector('h3')?.textContent); if (!titles[3]?.includes('sunrise, noon, and sunset') || !titles[6]?.includes('apparent solar time') || !titles[7]?.includes('shadow square')) throw new Error(`curriculum order is wrong: ${titles.join(' | ')}`); })()"
}
],
"expect": { "state": "ready" }
Expand Down Expand Up @@ -121,6 +124,15 @@
}
],
"expect": { "state": "ready" }
},
{
"label": "all-completed-lessons-launch",
"steps": [
{
"eval": "(async () => { const cases = [['solar.events.v1', 'Choose the date and place', 'front'], ['front.unequal-hours.v1', 'Understand temporal hours', 'front'], ['back.measure-altitude.v1', 'Understand the physical observation', 'back'], ['back.equation-time.v1', 'Distinguish the two kinds of time', 'back'], ['back.shadow-square.v1', 'Choose the known measurements', 'back'], ['back.unequal-hours.v1', 'Turn to the back', 'back']]; const settle = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); for (const [id, heading, face] of cases) { const start = document.querySelector(`[data-testid=\"start-${CSS.escape(id)}\"]`); if (!start) throw new Error(`missing start button for ${id}`); start.click(); await settle(); if (document.querySelector('#lesson-step-heading')?.textContent !== heading) throw new Error(`${id} did not open at its first step`); if (window.astrolabe.getState().face !== face) throw new Error(`${id} opened on the wrong face`); const all = [...document.querySelectorAll('button')].find((button) => button.textContent === 'All lessons'); if (!all) throw new Error(`cannot return from ${id}`); all.click(); await settle(); } })()"
}
],
"expect": { "state": "ready" }
}
]
}
23 changes: 7 additions & 16 deletions widgets/astrolabe-simulator/src/components/AstrolabeGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type AstrolabeState,
} from '../store';
import { animateAngle } from '../tutorial/animation';
import { LESSONS, FUTURE_TOPICS } from '../tutorial/catalog';
import { LESSONS } from '../tutorial/catalog';
import { evaluateCheckpoint } from '../tutorial/checkpoints';
import { composeTutorialSearch, parseTutorialSearch } from '../tutorial/url';
import type { Lesson, Snapshot } from '../tutorial/types';
Expand Down Expand Up @@ -136,26 +136,17 @@ export function AstrolabeGuide(): JSX.Element {
{tab === 'learn' ? <section id="learn-panel" role="tabpanel" aria-labelledby="learn-tab">
<h2>Astrolabe Guide</h2>
<p className="guide-intro">Choose a guided lesson to learn the instrument step by step.</p>
{[...new Set(LESSONS.map((item) => item.category))].map((category) => <section className="lesson-category" key={category}>
<h3>{category}</h3>
{LESSONS.filter((item) => item.category === category).map((item) => <article className="lesson-card" key={item.id}>
<h4>{item.title}</h4><p>{item.summary}</p>
<p className="lesson-meta">{item.steps.length} steps · {item.id.startsWith('back') ? 'Back' : 'Front'}</p>
<div className="lesson-list" aria-label="Lessons in course order">
{LESSONS.map((item, index) => <article className="lesson-card" key={item.id}>
<h3>{index + 1}. {item.title}</h3><p>{item.summary}</p>
<p className="lesson-meta">{item.steps.length} steps</p>
<button data-testid={`start-${item.id}`} onClick={(event) => start(item, event.currentTarget)}>Start lesson</button>
</article>)}
</section>)}
<h2>Future operations</h2>
{[...new Set(FUTURE_TOPICS.map((item) => item.category))].map((category) => <section className="lesson-category future-category" key={category}>
<h3>{category}</h3>
{FUTURE_TOPICS.filter((item) => item.category === category).map((item) => <article className="lesson-card future-card" key={item.title}>
<h4>{item.title}</h4><p>{item.prerequisite}</p>
<button disabled aria-disabled="true" title={item.prerequisite}>Planned lesson</button>
</article>)}
</section>)}
</div>
</section> : <Reference simulator={simulator} />}
</> : <section className="lesson-player" aria-labelledby="lesson-step-heading">
<button className="text-button" onClick={() => exit(false)}>All lessons</button>
<p className="lesson-breadcrumb">{lesson.category} · {lesson.title}</p>
<p className="lesson-breadcrumb">{lesson.title}</p>
{!complete && step && <>
<p className="lesson-progress">Step {stepIndex + 1} of {lesson.steps.length}</p>
<h2 id="lesson-step-heading">{step.title}</h2>
Expand Down
4 changes: 2 additions & 2 deletions widgets/astrolabe-simulator/src/components/Back.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function Back(): JSX.Element {
<line className="astro-back-axis" x1="-392" y1="0" x2="392" y2="0" />
<line className="astro-back-axis" x1="0" y1="-392" x2="0" y2="392" />

{visibility.shadowSquare && <g aria-label="Shadow square divided into twelve parts">
{visibility.shadowSquare && <g aria-label="Shadow square divided into twelve parts" data-tutorial-target="back.shadow-square">
<rect data-testid="shadow-square" className="astro-shadow-square" x={shadow.left} y={shadow.top} width={shadow.right - shadow.left} height={shadow.bottom - shadow.top} />
{shadow.verticals.map((x, part) => <line key={`shadow-v-${part}`} className="astro-shadow-grid" x1={x} y1={shadow.top} x2={x} y2={shadow.bottom} />)}
{shadow.horizontals.map((y, part) => <line key={`shadow-h-${part}`} className="astro-shadow-grid" x1={shadow.left} y1={y} x2={shadow.right} y2={y} />)}
Expand Down Expand Up @@ -163,7 +163,7 @@ export function Back(): JSX.Element {
))}
</g>}

{visibility.equationOfTime && <g aria-label="Equation-of-time curve">
{visibility.equationOfTime && <g aria-label="Equation-of-time curve" data-tutorial-target="back.equation-time">
<path className="astro-equation-curve" d={equationPath} />
<text className="astro-equation-label" x={equationLabel.x} y={equationLabel.y} text-anchor="middle"
aria-label="Equation of time">EOT</text>
Expand Down
2 changes: 1 addition & 1 deletion widgets/astrolabe-simulator/src/components/Plate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function Plate({ latitude, visibility }: PlateProps): JSX.Element {
{visibility.azimuths && <g clip-path="url(#above-horizon-clip)">
{azimuthDegrees.map((degrees) => <GeometryMark key={degrees} geometry={azimuth(latitude, 90 - degrees, ASTROLABE_R)} className="astro-azimuth" extent={rim} />)}
</g>}
{visibility.unequalHours && <g>
{visibility.unequalHours && <g data-tutorial-target="front.unequal-hours">
{UNEQUAL_HOUR_LINES.map((hour) =>
<path key={hour} className="astro-unequal-hour" d={unequalHourPath(latitude, hour, ASTROLABE_R)} />,
)}
Expand Down
17 changes: 1 addition & 16 deletions widgets/astrolabe-simulator/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ button, input, select {
.astrolabe-guide button:focus-visible { outline: 3px solid var(--input-focus); outline-offset: 2px; }
.astrolabe-guide h2 { margin-top: 10px; }
.astrolabe-guide h3 { margin: 18px 0 8px; font-size: 17px; }
.astrolabe-guide h4 { margin: 0; font-size: 16px; }
.lesson-card h3 { margin: 0; font-size: 16px; }
.guide-intro, .lesson-card p { color: var(--fg-secondary); }
.lesson-card {
margin-bottom: 10px;
Expand All @@ -260,20 +260,6 @@ button, input, select {
}
.lesson-card p { margin: 5px 0 10px; }
.lesson-meta, .lesson-progress, .lesson-breadcrumb, .target-note { color: var(--muted); font-size: 14px; }
.future-card {
color: var(--fg);
background: var(--card-bg);
border-color: var(--card-border);
}
/* Theme audits sample disabled controls directly: keep every surface fully
opaque and explicit. Never dim this card or one of its ancestors. */
.future-card button:disabled {
opacity: 1;
color: var(--fg-secondary);
background-color: var(--card-bg);
border-color: var(--card-border);
cursor: not-allowed;
}
.lesson-player h2:focus { outline: 3px solid var(--input-focus); outline-offset: 3px; }
.lesson-result { padding: 10px; border-left: 4px solid var(--accent); background: var(--card-bg); }
.lesson-actions, .interruption, .completion { display: flex; flex-wrap: wrap; gap: 8px; }
Expand Down Expand Up @@ -412,7 +398,6 @@ button, input, select {
}

@media (forced-colors: active) {
.future-card, .future-card button:disabled { color: GrayText; background: Canvas; border-color: GrayText; opacity: 1; }
.astrolabe-guide button:focus-visible { outline-color: Highlight; }
}

Expand Down
Loading
Loading