Imported from AlphaXT38G/notes_ba5 (
AGENTS.md). Install upstream withnpx skills add AlphaXT38G/notes_ba5. Copyright stays with the author.
AGENTS.md
Guide for anyone working on this repository, human or AI agent (Claude Code reads it through
CLAUDE.md). Read it before changing the build or the index page.
What this repository is
LaTeX notes for the BA5 IN semester at EPFL. build.py compiles every subject to PDF and generates
dist/index.html, an index page of all the PDFs. On every push to main, and every day at
04:00 UTC (so the "New" badges expire), the GitHub Pages workflow (.github/workflows/pages.yml)
runs the build and publishes dist/ at https://alphaxt38g.github.io/notes_ba5/.
The index page has grown a lot of personality: a flame mode, a jazz bar at night (closed by a
life-size Gigachad bartender at 00:45), a metal show at 01:00, an epic finale, a calm night with
crickets until 05:30, Christmas screens in December, a vintage cassette deck by day, easter eggs. Most of the code is there, in site/.
Repository layout
build.py entry point: compile subjects, then write dist/index.html
builder/ the Python behind build.py
config.py paths, subjects.json, constants of the index page
latex.py compiling a subject's LaTeX into dist/<subject>/
previews.py first-page thumbnails of the PDFs (poppler)
page.py dist/index.html: cards from dist/, assembled with site/
log.py coloured console output
site/ the index page (HTML, CSS, JS inlined into one dist/index.html, images)
template.html the HTML skeleton, with {{placeholders}}
manifest.json which CSS / JS files go in the page, IN ORDER
css/ one file per theme / screen
js/ one file per feature / screen
img/ images, copied as files to dist/img/ (too heavy to inline); bartender/
holds the closing bartender's layers (the Gigachad meme photo, dressed as a
bartender by an image generator, used as an illustration in this
non-commercial project)
audio/ the screens' music, mp3 files copied to dist/audio/: jazz, metal, epic,
calm (the night's forest) and jingle-bell-rock (Christmas); the day's
cassette deck plays them too
tools/bartender/ remakes the bartender's layers from source.webp (open it through a local
server, see below): cut-out, joints, bar counter and desk bell
subjects.json subjects: id, display name, exam rules, optional "weeks" and "color"
<subject>/ course.tex, cheatsheet.tex, week/weekNN.tex, extra PDFs...
_shared/ course.sty and cheatsheet.sty, shared by every course / cheat sheet
dist/ build output (git-ignored): PDFs, previews, index.html
Build and preview locally
python build.py # compile everything, then the index page
python build.py compsec swent # only some subjects (then the index page)
python build.py --index-only # only the index page, from the PDFs already in dist/
python -m http.server 8765 --directory dist # then open http://localhost:8765/
- Windows: prefix the build with
PYTHONIOENCODING=utf-8, or the console cannot print ✔ / →. - Optional tools:
pdftoppmandpdftotext(poppler) make the thumbnails; without them the page shows names only.nodechecks the page's JavaScript syntax at every build and points to the faultysite/jsfile and line. Every build also warns about JavaScript too new for older phone browsers (see "Phones and older browsers" below), node or not. - Never edit
dist/: it is regenerated by every build. - Some documents do not compile today because of missing images (e.g.
elec/course.tex,respsoft/course.tex,compsec/cheatsheet.tex). That is expected until the images are added.
Remaking the closing bartender
His layers in site/img/bartender/ come from tools/bartender/source.webp. To remake them (new
photo, new cut, new props), serve the repository root and open the tool:
python -m http.server 8777
then http://localhost:8777/tools/bartender/: it shows the layers stacked on red (any leftover
background shows) with a link to save each one into site/img/bartender/. The outlines of the cut
(head, forearm, protected skin) are in cut.js, in photo pixels; the counter and the bell, drawn
for the WebGL relief renderer (relief.js) and lit like the photo, are in props.js.
Trying the screens at any time
The night screens follow the clock. Force one with the URL (?night=, the old name, still works):
| URL | Screen |
|---|---|
?screen=0 |
the day, whatever the time |
?screen=1 |
the first scheduled screen (the jazz bar) |
?screen=evening / ?screen=closing |
jazz bar / bar closing (the bartender comes ~9 s in, once the sign is dark) |
?screen=metal |
metal show (does not end by itself when forced: the song plays on and on) |
?screen=epic |
epic finale (until "Back to normal") |
?screen=calm |
calm night (no dawn when forced outside 04:45 – 05:30) |
?screen=christmas / ?screen=christmas-night |
Christmas day / Christmas night |
Easter eggs: 20 clicks on 🔥 within a minute (metal show), the Konami code ↑↑↓↓←→←→BA, yeti typed
anywhere but the search field (Christmas day or night by the clock, any time of the year; outside
the Christmas period, typed on a Christmas screen it goes back to the normal one; typed in a jazz
bar opened over Christmas, it brings Christmas back), a random surprise banner on some visits, and
on both Christmas screens (day and night) a small golden sax hidden among the tree ornaments that
opens the jazz bar; a little fir in the bar's bottom right corner brings Christmas back. A red
guitar standing behind the green gift under the tree starts the metal show (then Christmas comes
back), and a firefly jar swinging under the garland opens the calm night (like peace, but the
fir shows there too and brings Christmas back). armstrong typed anywhere but the search field
opens the jazz bar at any time (over Christmas and the calm night too, but not over the shows);
typed again in the bar it goes back to
the normal screen of that moment, by the clock alone: it drops the armstrong and yeti eggs, the
sax and a forced ?screen=, so it shows the day (or the calm night), the bar itself at bar time
(21:30 – 01:00), or Christmas during the Christmas period. peace typed anywhere but the search
field brings the calm night at any time, like yeti (over the day, the jazz bar and Christmas, not
over the shows, which it comes back after); yeti or armstrong typed after it wins, and typed
again on the calm night it goes back to the normal screen of that moment (on the calm night of the
clock, it does nothing). normal typed anywhere but the search field brings back the day at any
time, over every screen, the shows included: it drops every other egg and a forced ?screen=, and
keeps the day even when the clock brings a screen; an egg typed or clicked after it wins, and typed
again it lets the clock decide.
How the index page is built
builder/page.py makes one card per subject from the PDFs in dist/, then fills
site/template.html:
{{css}}: every file ofmanifest.json→"css", concatenated in that order. Later files win over earlier ones for rules of the same weight, so the order is part of the design (see below). The build writesinset:astop/right/bottom/left(older browsers).{{js}}: every file ofmanifest.json→"js", in that order, each in its own<script>at the end of<body>. They share one scope: a file can use what an earlier file defines (const,let, functions). Code that runs at load can only use what is defined above it; code in event handlers runs later and can use anything. A file that fails (a syntax or an API the browser lacks) stops alone, with what uses it;ui.js, first, always keeps the search, the theme and the view switch working.{{head_js}}:site/js/head.js, in<head>, applies the saved light/dark theme and view before the first paint.
The result is a single self-contained dist/index.html: no request for CSS or JS. Only images
(site/img/, e.g. the bartender's head) are separate files, copied to dist/img/.
JS files, in load order
| File | What it does |
|---|---|
ui.js |
light/dark toggle (root = <html>), Names / Previews switch, search |
flames.js |
🔥 flame mode, setFire, crowded() (anti pile-up helper), the 20-click easter egg |
surprise.js |
random surprise banner |
blackhole.js |
createBlackHole(): the Konami black hole, ray-traced in WebGL (light bent by gravity, disk with Doppler beaming, lensed stars), on two canvases so the page falls between its sky and its disk |
supernova.js |
createSupernova(): the multi-stage supernova that ends the Konami black hole (flash, shock wave, ejecta, nebula, pulsar), one WebGL canvas over the page |
konami.js |
Konami code: black hole (CSS drawing as a fallback without WebGL) swallowing the page letter by letter, then its boxes and the music player (the Christmas garland, jar, sax and guitar fade out, konami.css; they stay hidden during the party theme), until it overheats, implodes and explodes in a supernova; confetti, party theme |
screens.js |
the screen engine: SCREENS, SCHEDULE, OVERRIDES, everySecond, applyScreen(); screenTrack() / preloadTrack() / dropTrack() for the screens' music |
jazz.js |
jazz bar screens (evening, closing): smoke, notes, turntable playing audio/jazz.mp3, neon sign going out |
closing.js |
closing time: the life-size bartender behind his bar (photo layers from img/bartender/: head nodding, forearm tapping the desk bell, chest breathing), swinging lamp, desk bell "ding" (Web Audio), typed speech, chalkboard with countdown, "Good night" |
fx.js |
the effects canvas of every screen: fxLayers (by <body> class), blob(), the animation loop |
christmas.js |
Christmas screens: date override and yeti code, garland, winter scene (mountains and chalets, moon, tree and gifts drawn in volume with a shared bow3d and giftTag, snowman), night lights canvases (tree, garland halos), snow, sleigh, music (Jingle Bell Rock or the YouTube Christmas radio, swapped with the sleeve behind the turntable, or paused), hidden sax and guitar (the guitar draws the gifts in front of it again over itself, to stand behind them while staying clickable), firefly jar swinging under the garland (a pendulum pushed by the wind, the mouse and scrolling, asleep once settled) |
armstrong.js |
armstrong code: the jazz bar at any time (after christmas.js to win over it, before metal.js so the shows still win); typed again, back to the clock's screen |
metal.js |
metal show: lightning, chains, fire, silhouettes, guitar amp and song (played 3 times) |
epic.js |
epic finale: blue fire, motes, rune circle, the "More epicness ?" medallion and the summoning before the finale (overlay, flash timed to the end of the show, Web Audio whoosh and boom), "Back to normal" |
calm.js |
calm night after the metal show: moon of tonight, hills, mist, dawn, stars, shooting stars, fireflies, night forest sounds (audio/calm.mp3, quieter), dawn birds made with Web Audio, firefly jar; the peace code (the last override: the calm night at any time; startPeace(), also called by the Christmas jar) |
normal.js |
normal code: the day at any time, over every screen (the very last override, after calm.js); typed again, back to the clock's screen |
tapes.js |
the day's cassette deck (no screen only): an early-80s hi-fi deck (VU meters, LED counter, piano keys ⏏ ◀◀ ▶❚❚ ▶▶ and the station key) whose music comes in bands (TAPE_BANDS): Lofi Girl mixes, then the screens' music (jazz, late night, metal, epic, Christmas), then Radio Impuls live. A tape is a YouTube video (hidden <iframe> driven by postMessage), an mp3 of site/audio/ or a live stream (<audio>). The station key goes to the next band, ⏏ or the next tape's case behind the deck to the next tape of the band (door opens, tape out, new one in, door closes); never plays by itself, the tape is remembered |
start.js |
last: first applyScreen(), then the every-second clock check |
CSS files, in cascade order
base.css (layout, cards, search, previews) → flames.css → surprise.css → jazz.css → closing.css →
metal.css → epic.css → christmas.css → calm.css → tapes.css → screens.css (effects canvas, view transitions) → konami.css.
The Konami file is last on purpose: during its animation body.rift .wrap must win over every
theme's .wrap rule.
Screens
A screen is a special look the whole page takes for a while. The engine is in
site/js/screens.js; each screen registers itself from its own file:
SCREENS.name = {
className: "...", // class put on <body> while it runs (screens may share one)
subtitle: "...", // replaces the subtitle: string, function returning one, or null
transition: "...", // view transition used to enter it: CSS [data-vt=...]
normal: true, // optional: the usual look of its hours, replaced like the day (calm night)
};
SCHEDULE.push([from, to, "name"]); // clock slot in seconds since midnight (to < from wraps)
OVERRIDES.push(phase => phase); // event-driven screens (easter eggs), applied in order
everySecond.push(() => { ... }); // work to do every second
What happens when the screen changes (applyScreen(), called every second by start.js):
body[data-phase]becomes the screen's name (removed during the day).- The subtitle changes, and the flame mode is turned off (too heavy on top of a screen).
<body>classes are toggled: those that end first, then those that start. For each one aCustomEventnamed after the class is sent ondocument(detail: true / false).- A
phaseevent is sent (detail: the screen's name, ornull). - All of this happens inside a View Transition (
html[data-vt]= the screen'stransition, orday), except on first load, with reduced motion, or in browsers without the API.
Current screens:
| Screen | Class | When | Files |
|---|---|---|---|
evening |
night |
21:30 – 00:45, or the armstrong code |
jazz.js, jazz.css, armstrong.js |
closing |
night |
00:45 – 01:00 | jazz.js, jazz.css, closing.js, closing.css |
metal |
metal |
01:00:00 – 01:01:51 (the song 3 times), or 20 flame clicks, or the Christmas guitar | metal.js, metal.css |
epic |
epic |
after a metal show, if "More epicness ?" was clicked | epic.js, epic.css |
calm |
calm |
01:01:51 – 05:30, unless the epic finale runs (a normal screen), or the peace code, or the Christmas jar |
calm.js, calm.css |
christmas |
christmas |
Dec 10 – Jan 3, 06:00 – 18:00 (replaces the day) | christmas.js, christmas.css |
christmas-night |
christmas |
Dec 10 – Jan 3, 18:00 – 06:00 (replaces the day, the jazz bar and the calm night) | christmas.js, christmas.css |
The Christmas screens depend on the date, so they are an override rather than a SCHEDULE slot:
it only rewrites the normal screens (no screen, the calm night: isNormal() in screens.js) and
the jazz screens, and christmas.js is loaded before metal.js,
so the metal show (01:00 or easter egg) and the epic finale still take over. A forced ?screen=
turns it off, except ?screen=christmas / ?screen=christmas-night themselves and the yeti easter egg.
Adding a new screen, step by step
Example: a calm "dawn" screen from 06:00 to 07:00.
-
site/js/dawn.js: register it, then react to its class event.// ── Dawn (screen "dawn", body.dawn) ── SCREENS.dawn = { className: "dawn", subtitle: "Good morning. Coffee first.", transition: "day" }; SCHEDULE.push([6 * 3600, 7 * 3600, "dawn"]); // Extras that exist only during the screen: create them on, remove them off. const sun = document.createElement("div"); sun.id = "sun"; const setDawn = on => on ? document.body.append(sun) : sun.remove(); document.addEventListener("dawn", e => setDawn(e.detail)); -
site/css/dawn.css: everything scoped underbody.dawn, plus its own reduced-motion block./* Dawn (body.dawn, 06:00 - 07:00): soft pink sky, sun rising behind the page. Every colour token is set: the visitor may be in light or dark mode. */ body.dawn{--bg:#fff4ee;--fg:#3a2414;--card:#fff;--line:#f0d6c8;--mut:#8a6a5a;--acc:#d9622b;--acc-bg:#ffe6da;--new:#1d7a46;--new-bg:#dcf3e5} body.dawn::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;background:linear-gradient(#ffd9c7,transparent 60%)} body.dawn .wrap{position:relative;z-index:1} #sun{position:fixed;left:50%;bottom:-6rem;width:12rem;height:12rem;border-radius:50%;background:#ffcf70;animation:rise-sun 20s ease-out forwards} @keyframes rise-sun{to{transform:translateY(-40vh)}} /* Reduced motion */ @media(prefers-reduced-motion:reduce){#sun{animation:none}}For a new view transition, add
[data-vt=dawn]::view-transition-old(root)/-new(root)rules and usetransition: "dawn". -
site/manifest.json: addjs/dawn.jsafterscreens.jsand beforejs/start.js(afterjs/fx.jstoo if it draws on the effects canvas, step 4: it needsfxLayersat load, likecalm.js), andcss/dawn.cssbeforecss/screens.css. -
Canvas effects (optional): push layers with
mode= the screen's class;fx.jsstarts the canvas while that class is on. The canvas (#metalfx) is over the page; for a sky behind the courses, put it behind (body.dawn #metalfx{z-index:0}, seecalm.css).fxLayers.push({ mode: "dawn", update(dt) {}, draw(ctx) {}, reset() {}, resize() {} }); -
Event-driven screens (optional): instead of
SCHEDULE, push an override that returns the screen's name while it must run (seemetal.jsfor an easter egg,epic.jsfor a screen queued by a button), then callapplyScreen()to switch at once. -
Normal screens (optional): a screen that is simply the usual look of its hours, like the calm night after the metal show, says
normal: true. Christmas and thearmstrongcode then replace it as they replace the day (isNormal()inscreens.js). -
Build, open
http://localhost:8765/?screen=dawn, check the console has no errors, and check the switch from and to the day with a mocked clock if the screen is scheduled.
Good practices
Code
- Comment the why, like the rest of the code: what a block does, why it is done that way. Every file starts with a header saying what it is about.
- Keep files focused: one theme or screen per CSS file, one feature or screen per JS file. Put shared helpers where every user is below them in the manifest.
- Scope screen CSS under
body.<class>, and give each file its own@media(prefers-reduced-motion:reduce)block for its animations. - A screen sets every colour token (
--bg --fg --card --line --mut --acc --acc-bg --new --new-bg, seebase.css): the visitor may be in light or dark mode, and a token left out keeps that mode's value (e.g. light text on a light background). - localStorage can throw (private mode): always
try { ... } catch (e) {}. - Sound needs a user gesture: players start muted when there was none, and unmute on the
first click (see
jazz.js,metal.js); a Web Audio context waits suspended and is resumed on the first click (see the dawn birds incalm.js). Music is an mp3 insite/audio/, played withscreenTrack()(screens.js, or loaded ahead withpreloadTrack()when a show is coming), never put in the page, and stopped withdropTrack()when its screen ends (the day's cassette deck also plays a radio's live stream,tapes.js, the same way). The YouTube players left (the Christmas radio, the day's cassettes intapes.js) are hidden<iframe>s driven bypostMessagecommands; a fresh player ignores commands until ready.
Phones and older browsers
Phones often run browsers built on an older Chromium (Samsung Internet, makers' browsers, old
Android WebViews). One syntax they do not know makes the browser reject a whole <script>: a
||= in ui.js once left a phone with no search, no theme and no view switch.
- Write JavaScript for Chromium 73 / Safari 12.1 (2019): no
?.,??,||=/&&=/??=,.at(),replaceChildren,roundRect,replaceAll,structuredClone, private fields... Usea && a.b,x = x || y,arr[arr.length - 1],el.textContent = "". The build lists any of them (TOO_NEWinbuilder/page.py); add a pattern there for any new API you avoid. - Guard optional APIs (
navigator.userActivation,AudioContext,startViewTransition, WebGL): test them before use, as the code already does. - Touch: controls are at least 44 px high on touch screens (
pointer:coarseinbase.css), with no tap delay. Nothing may cover the header controls; fixed widgets go in the corners, and phones get room under the last card for them (screens.css). - Check a phone-sized window (375 px wide) after a page change: the search, the theme and
the Names / Previews switch must work on every screen (
?screen=...).
Performance (the page runs heavy effects; keep scrolling smooth)
- Animate only
transformandopacity. To animate a glow, put it on a pseudo-element and animate its opacity, notbox-shadow. - Never use
background-attachment: fixed: use aposition: fixedpseudo-element instead (body.<class>::before). - Anything spawned on a timer must be capped and must not pile up in background tabs: use
crowded(layer, max)fromflames.js. - Draw many moving things on the effects canvas (
fx.js) rather than as DOM elements. The canvas is 1x even on high-density screens; pre-render expensive shapes once (see the rune circle inepic.js) and avoidshadowBlurin per-frame drawing. - Heavy SVG filters (textures, blurs, grain) are fine in an SVG shown as an
<img>: it is rendered once into a bitmap, and animating the<img>never re-runs them. - Inside an inline
<svg>, an animated element is restyled and redrawn on the main thread on every frame, with what lies under it (and any filter on it). A few are fine; dozens (lights that twinkle each at its own pace) are not: draw them on a canvas, as the Christmas garland's halos (paintGarlandGlowinchristmas.js), and keep blur filters off what animates. - A photo that moves (the closing bartender) is cut into layers at its joints, all at the photo's
size so they stack exactly, and only
transform/translate/scaleanimate them. Each moving layer reaches under the one that hides its joint (the forearm goes up under the rolled sleeve), and the layer behind loses that part, so a move never opens a gap nor shows a ghost. - All scripts share one scope: prefix new top-level names if they could clash with another file's.
- Remove a screen's extras (elements, timers, players) when its class turns off.
Workflow
- Branch from an up-to-date
main(feat/...,fix/...,refactor/...), open a PR. - Commits: Conventional Commits in English, one
commit per change,
type(scope): summary. Types used here:feat,fix,style,perf,refactor,docs,build,ci,chore. Scopes:index/site(the page),build, or a subject and document (compsec-week05,respsoft-cheatsheet). - Before committing a page change:
python build.py --index-only(the JS syntax check must say OK), open the page, try the screens concerned with?screen=..., check the browser console has no errors, and check a phone-sized window. - Keep
README.md(for readers) and this file (for contributors) up to date with what changes.
LaTeX notes
- Weekly notes are
week/weekNN.texstandalone documents (seecompsec/week/); they appear in the "weekly notes" section of their subject, with a progress bar ("weeks"insubjects.jsonsets its length, 14 by default). swentis the exception: its weeks are chapters\inputbycourse.tex(weeks/week1a.tex,week1b.tex...).builder/latex.pybuilds one PDF per week from generated_weekNN.texfiles (git-ignored). Name new chaptersweekN+ optional letter.- Courses and cheat sheets load
_shared/course.sty/_shared/cheatsheet.sty(layout, boxes, footer with the repository link). Change the look of all of them there. - A PDF without a matching
.tex(e.g.respsoft/cheatsheets/*.pdf) is copied as is.
