Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
759dd07
katex vendoring + rendering
mathematicalmichael Mar 7, 2026
274420c
capture 2 more slides
mathematicalmichael Mar 7, 2026
f4b6764
increase font sizes
mathematicalmichael Mar 7, 2026
7976613
text wrapping
mathematicalmichael Mar 7, 2026
0fc1062
sub-list formatting
mathematicalmichael Mar 7, 2026
d3550f0
flex fonts
mathematicalmichael Mar 7, 2026
3e09ae4
tick mark styling
mathematicalmichael Mar 7, 2026
32a38f3
diff sizes
mathematicalmichael Mar 7, 2026
daf0a94
try to support wider screens
mathematicalmichael Mar 7, 2026
492e0a3
use script for screenshots
mathematicalmichael Mar 7, 2026
4d5a41c
use one chromium instance
mathematicalmichael Mar 7, 2026
3f1fa88
font handling
mathematicalmichael Mar 7, 2026
d705d6d
fix approach to prior known good
mathematicalmichael Mar 7, 2026
6cfd4b2
fix screenshot script
mathematicalmichael Mar 7, 2026
64b8d51
working on mobile support
mathematicalmichael Mar 7, 2026
af65f4e
scaling tables
mathematicalmichael Mar 7, 2026
a4e4b08
keep scaling up
mathematicalmichael Mar 7, 2026
c8ce351
blacksmith runner
mathematicalmichael Mar 7, 2026
f07529f
table relative scaling
mathematicalmichael Mar 7, 2026
73412d8
some math font scaling
mathematicalmichael Mar 7, 2026
d83d113
messy classes but consistent table formatting
mathematicalmichael Mar 7, 2026
ee4fe60
standalone table class
mathematicalmichael Mar 7, 2026
93af9a1
parallel screen captures (8)
mathematicalmichael Mar 7, 2026
be70479
back to 8 workers remotely
mathematicalmichael Mar 7, 2026
4a53454
table sizing again
mathematicalmichael Mar 7, 2026
af4304c
borders for table now scale up
mathematicalmichael Mar 7, 2026
c97c52f
RELATIVE SCALING ROUND 3
mathematicalmichael Mar 7, 2026
ecdc1d6
screenshot viewer
mathematicalmichael Mar 7, 2026
a937284
more attempts at consistency
mathematicalmichael Mar 7, 2026
0abb311
use full width for lists
mathematicalmichael Mar 7, 2026
b05d636
header and footer sizing
mathematicalmichael Mar 7, 2026
1cb7f58
removing height cap
mathematicalmichael Mar 7, 2026
9d77c65
present math after code fence with less margin
mathematicalmichael Mar 7, 2026
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
211 changes: 21 additions & 190 deletions .github/workflows/capture.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,29 @@ on:
type: choice
options:
- '0.5'
- '0.75'
- '1'
- '1.25'
- '1.5'
- '3'
- '2'
num_workers:
description: Number of Chromium workers to run in parallel
required: false
default: '8'
type: choice
options:
- '1'
- '2'
- '4'
- '6'
- '8'

permissions:
contents: write

jobs:
screenshot:
runs-on: ubuntu-latest
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -47,201 +60,19 @@ jobs:
- name: Start server and take screenshots
env:
SCREENSHOT_ZOOM_LEVEL: ${{ inputs.zoom_level || '1' }}
SCREENSHOT_NUM_WORKERS: ${{ inputs.num_workers || '4' }}
SCREENSHOT_OUT_DIR: ${{ github.workspace }}/.screenshots
run: |
# Start static server in background
bunx --bun serve . -p 1313 > /dev/null 2>&1 &
SERVER_PID=$!

# Wait for server to be ready
echo "Waiting for server to start..."
for i in {1..30}; do
if curl -s http://localhost:1313/ > /dev/null 2>&1; then
echo "Server is ready!"
break
fi
sleep 1
done

# Create screenshot directories and capture slides
node << 'EOF'
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const zoomLevel = Number(process.env.SCREENSHOT_ZOOM_LEVEL || '1');

if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) {
throw new Error(`Invalid SCREENSHOT_ZOOM_LEVEL: ${process.env.SCREENSHOT_ZOOM_LEVEL}`);
}

// URLs to capture: [url, folderName] or [url, folderName, waitTimeMs]
// Optional waitTimeMs: wait after load before screenshot (for animated slides)
// Main slides 1-17, plus basement slides at 8 and 16
const urls = [
['/', 'slide-01'],
['/#/2', 'slide-02'],
['/#/3', 'slide-03'],
['/#/4', 'slide-04'],
// ['/#/5', 'slide-05'],
// ['/#/6', 'slide-06'],
// ['/#/7', 'slide-07'],
['/#/8', 'slide-08-01'],
['/#/8/2', 'slide-08-02'],
// ['/#/9', 'slide-09'],
['/#/10', 'slide-10'],
['/#/11', 'slide-11'],
['/#/12', 'slide-12'],
// ['/#/13', 'slide-13'],
// ['/#/14', 'slide-14'],
// ['/#/15', 'slide-15'],
['/#/16', 'slide-16-01'],
['/#/16/2', 'slide-16-02'],
['/#/16/3', 'slide-16-03'],
// ['/#/17', 'slide-17'],
];

// Define resolutions: [width, height, label]
const resolutions = [
// Mobile
[375, 667, 'iphone-se'],
[390, 844, 'iphone-12-13'],
[430, 932, 'iphone-14-15-16-17-pro-max'],
// Mobile Landscape
[667, 375, 'iphone-se-landscape'],
[844, 390, 'iphone-12-13-landscape'],
[932, 430, 'iphone-14-15-16-17-pro-max-landscape'],
// Tablet
[768, 1024, 'ipad-portrait'],
[1024, 768, 'ipad-landscape'],
// Desktop
[1280, 720, 'desktop-1280x720'],
[1366, 768, 'desktop-1366x768'],
[1440, 900, 'desktop-1440x900'],
[1920, 1080, 'desktop-1920x1080'],
[2560, 1440, 'desktop-2560x1440'],
];

async function takeScreenshots(url, folder, waitTime = 0) {
const browser = await chromium.launch();
const screenshots = [];
const fullUrl = `http://localhost:1313${url}`;

if (waitTime > 0) {
// Load once, wait for animation, then resize for each resolution
console.log(`Loading ${url} at ${zoomLevel}x zoom and waiting ${waitTime}ms for animation...`);
const page = await browser.newPage();
const [firstWidth, firstHeight, firstLabel] = resolutions[0];
await page.setViewportSize({ width: firstWidth, height: firstHeight });
await page.goto(fullUrl, { waitUntil: 'networkidle' });
await page.waitForTimeout(500);
await page.evaluate((zoom) => {
document.documentElement.style.zoom = String(zoom);
}, zoomLevel);
await page.waitForTimeout(100);
await page.waitForTimeout(waitTime);

// Now take screenshots at all resolutions by resizing
for (const [width, height, label] of resolutions) {
console.log(`Taking ${width}x${height} (${label}) for ${url} at ${zoomLevel}x zoom`);
await page.setViewportSize({ width, height });
await page.evaluate((zoom) => {
document.documentElement.style.zoom = String(zoom);
}, zoomLevel);
// Small delay to let layout adjust after resize
await page.waitForTimeout(100);

const filename = `${width}x${height}-${label}.png`;
const filepath = path.join(folder, filename);
await page.screenshot({ path: filepath, fullPage: false });
screenshots.push({ filename, width, height, label });
}

await page.close();
} else {
// Reload for each resolution
for (const [width, height, label] of resolutions) {
console.log(`Taking ${width}x${height} (${label}) for ${url} at ${zoomLevel}x zoom`);
const page = await browser.newPage();
await page.setViewportSize({ width, height });
await page.goto(fullUrl, { waitUntil: 'networkidle' });
await page.waitForTimeout(500);
await page.evaluate((zoom) => {
document.documentElement.style.zoom = String(zoom);
}, zoomLevel);
await page.waitForTimeout(100);

const filename = `${width}x${height}-${label}.png`;
const filepath = path.join(folder, filename);
await page.screenshot({ path: filepath, fullPage: false });
await page.close();

screenshots.push({ filename, width, height, label });
}
}

await browser.close();
return screenshots;
}

function getFolderTitle(folderName) {
return folderName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}

async function generateReadme(folder, screenshots) {
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0] + 'Z';
const readmePath = path.join(folder, 'README.md');

let content = `# ${getFolderTitle(folder)}\n\n`;
content += `Generated: ${new Date().toISOString()}\n\n`;
content += `| Resolution | Device | Screenshot |\n`;
content += `|------------|--------|------------|\n`;

for (const { filename, width, height, label } of screenshots) {
content += `| ${width} × ${height} | ${label} | ![${label}](${filename}?${timestamp}) |\n`;
}

fs.writeFileSync(readmePath, content);
}

(async () => {
try {
const allFolders = [];
console.log(`Using screenshot zoom level: ${zoomLevel}x`);

for (const entry of urls) {
const [url, folderName, waitTime = 0] = entry;
fs.mkdirSync(folderName, { recursive: true });
allFolders.push(folderName);

const waitMsg = waitTime > 0 ? ` (wait ${waitTime}ms)` : '';
console.log(`=== Capturing ${folderName} for ${url}${waitMsg} ===`);
const screenshots = await takeScreenshots(url, folderName, waitTime);
await generateReadme(folderName, screenshots);
console.log(`Captured ${screenshots.length} screenshots for ${folderName}`);
}

console.log('All screenshots saved successfully');
fs.writeFileSync('_screenshot_folders.json', JSON.stringify(allFolders));
} catch (error) {
console.error('Error taking screenshots:', error);
process.exit(1);
}
})();
EOF

# Stop server
kill $SERVER_PID 2>/dev/null || true
./scripts/capture-screenshots.sh

- name: Create orphan branch and commit screenshots
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

FOLDERS=$(node -e "console.log(require('./_screenshot_folders.json').join(' '))")
rm _screenshot_folders.json
mv $FOLDERS /tmp/
FOLDERS=$(bun -e "console.log(JSON.parse(require('fs').readFileSync('.screenshots/_screenshot_folders.json', 'utf8')).join(' '))")
rm .screenshots/_screenshot_folders.json
mv .screenshots/slide-* /tmp/

git checkout main || git checkout master || true
git branch -D screenshots 2>/dev/null || true
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.screenshots/
node_modules
bun.lock
package.json
*.cjs
66 changes: 56 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Reveal.js runtime assets are vendored in-repo for offline use.
- `index.html`: Reveal.js bootstrap, markdown loading, and custom slide behavior.
- `custom.css`: presentation theme and layout styling.
- `vendor/reveal.js/`: local Reveal.js CSS/JS/plugin assets used by `index.html`.
- `vendor/katex/`: slim local KaTeX runtime used for offline math rendering.
- `scripts/vendor-katex.sh`: idempotent vendoring script for the KaTeX runtime subset.
- `scripts/capture-screenshots.sh`: local multi-resolution screenshot capture for visual validation.

## Run In Dev

Expand All @@ -20,17 +23,60 @@ bunx --bun serve . -p 1313

Then open the local URL printed by `serve` in your browser.

## Refresh Vendored Reveal Assets
## Capture Screenshots Locally

To validate typography/layout changes across the same resolution set used in CI:

```bash
mkdir -p vendor/reveal.js/dist/theme vendor/reveal.js/plugin/markdown vendor/reveal.js/plugin/highlight
wget -q -O vendor/reveal.js/dist/reveal.css https://cdn.jsdelivr.net/npm/reveal.js@5/dist/reveal.css
wget -q -O vendor/reveal.js/dist/theme/black.css https://cdn.jsdelivr.net/npm/reveal.js@5/dist/theme/black.css
wget -q -O vendor/reveal.js/dist/reveal.js https://cdn.jsdelivr.net/npm/reveal.js@5/dist/reveal.js
wget -q -O vendor/reveal.js/plugin/markdown/markdown.js https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/markdown/markdown.js
wget -q -O vendor/reveal.js/plugin/highlight/highlight.js https://cdn.jsdelivr.net/npm/reveal.js@5/plugin/highlight/highlight.js
SCREENSHOT_ZOOM_LEVEL=1 ./scripts/capture-screenshots.sh
```

This writes slide image folders plus `_screenshot_folders.json` into `.screenshots/` by default.
The script aligns with CI by ensuring the `playwright` package is installed locally before capture, then installing Chromium.
Override `SCREENSHOT_OUT_DIR` to capture elsewhere, and `SCREENSHOT_PORT` if `1313` is already in use.

## Authoring Math

Display math can be written using fenced `math` blocks in `present.md`:

````md
```math
\int_0^\infty e^{-x^2} \, dx = \frac{\sqrt{\pi}}{2}
```
````

`index.html` rewrites these fences to KaTeX display-math delimiters before Reveal parses the deck, so slide authoring stays markdown-native while rendering still uses Reveal's math plugin.

Inline math also works with standard KaTeX delimiters such as `$x^2$`, `\(...\)`, and `\[...\]`.

## Refresh Vendored Assets

```bash
mkdir -p vendor/reveal.js/dist/theme vendor/reveal.js/plugin/markdown vendor/reveal.js/plugin/highlight vendor/reveal.js/plugin/math
curl -fsSL https://cdn.jsdelivr.net/npm/reveal.js@5.2.1/dist/reveal.css -o vendor/reveal.js/dist/reveal.css
curl -fsSL https://cdn.jsdelivr.net/npm/reveal.js@5.2.1/dist/theme/black.css -o vendor/reveal.js/dist/theme/black.css
curl -fsSL https://cdn.jsdelivr.net/npm/reveal.js@5.2.1/dist/reveal.js -o vendor/reveal.js/dist/reveal.js
curl -fsSL https://cdn.jsdelivr.net/npm/reveal.js@5.2.1/plugin/markdown/markdown.js -o vendor/reveal.js/plugin/markdown/markdown.js
curl -fsSL https://cdn.jsdelivr.net/npm/reveal.js@5.2.1/plugin/highlight/highlight.js -o vendor/reveal.js/plugin/highlight/highlight.js
curl -fsSL https://cdn.jsdelivr.net/npm/reveal.js@5.2.1/plugin/math/math.js -o vendor/reveal.js/plugin/math/math.js
./scripts/vendor-katex.sh
```

The KaTeX script vendors only the runtime files Reveal needs:

- `dist/katex.min.js`
- `dist/katex.min.css`
- `dist/contrib/auto-render.min.js`
- `dist/fonts/*.woff2`
- `dist/fonts/*.woff`
- `LICENSE`

Set `KATEX_VERSION` to override the pinned default when refreshing, for example:

```bash
KATEX_VERSION=0.16.37 ./scripts/vendor-katex.sh
```

## Footer Text

# Footer Text
Line 294 of `index.html` can be edited to change the footer text from `present.md` to anything else.
It is there that the styling of the pagination is also handled.
Edit the `counter.textContent = \`${current} / ${total} | present.md\`;` line in `index.html` to change the footer text from `present.md` to something else.
Loading