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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ log says why the algorithm accepted or rejected each candidate.
**[Try it in your browser →](https://lolodawit.github.io/ESU-Algorithm/)** — no install,
the algorithm runs client-side.

![The visualizer running: the search tree grows step by step while the matching vertices light up in the graph](docs/demo.gif)
![The visualizer running in a browser: the search tree grows step by step, results turn green, and choosing a shape draws every subgraph that has it](docs/demo.gif)

## Running it

Expand Down
Binary file modified docs/demo-poster.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/demo.mp4
Binary file not shown.
1 change: 1 addition & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
dist/
test-results/
playwright-report/
recording/
8 changes: 8 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ background, or clip the shape list so its contents scroll out of sight, and all
94 unit tests still pass while the browser tests fail. Both of those are bugs
that actually shipped.

## The moving pictures

`npm run demo:record` drives the published page in Chromium and turns the
recording into `docs/demo.mp4`, `docs/demo.gif` and a poster frame. The assets
it replaces were a hand-made screen recording of the desktop app, which went
out of date the moment the tree layout changed and stayed wrong for weeks.
Re-running the script is all that keeping them honest now takes.

Visual regression by screenshot comparison is deliberately not used: fonts
render differently on macOS and on the Linux runner, so pixel diffs would fail
for reasons that have nothing to do with the change.
Expand Down
28 changes: 28 additions & 0 deletions web/browser/encode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/sh
# Turns the recording into the three assets the site and README use.
#
# Run through `npm run demo:record`, which records first. Needs ffmpeg.
set -e
cd "$(dirname "$0")/.."

test -f recording/demo.webm || { echo "no recording; run npm run demo:record"; exit 1; }

# The site: full length, full size, small enough to stream on arrival.
ffmpeg -y -v error -i recording/demo.webm -an \
-vf "fps=30,scale=1120:-2:flags=lanczos" \
-c:v libx264 -crf 24 -preset slow -pix_fmt yuv420p -movflags +faststart \
../docs/demo.mp4

# The poster: a frame from near the end, where the tree is complete.
ffmpeg -y -v error -ss 15.4 -i recording/demo.webm -frames:v 1 \
-vf "scale=1120:-2:flags=lanczos" -q:v 3 ../docs/demo-poster.jpg

# The README: GitHub will not autoplay a video from a repository path, so this
# one has to be a GIF. Trimmed past the setup, slightly quickened, and cut to
# 64 colours -- the interface is flat colour, so the palette costs nothing.
ffmpeg -y -v error -ss 3.2 -i recording/demo.webm \
-vf "setpts=0.7*PTS,fps=10,scale=780:-2:flags=lanczos,split[a][b];\
[a]palettegen=max_colors=64:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=3" \
../docs/demo.gif

ls -la ../docs/demo.mp4 ../docs/demo.gif ../docs/demo-poster.jpg
92 changes: 92 additions & 0 deletions web/browser/record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { chromium } from '@playwright/test';
import { mkdir, readdir, rename, rm } from 'node:fs/promises';
import { join } from 'node:path';

/**
* Records the demo for the README and the project page.
*
* The asset it replaces was a hand-made screen recording of the desktop app,
* which went out of date the moment the tree layout changed and nobody
* noticed for weeks. This drives the real published page, so re-running it is
* all that keeping the picture honest takes.
*
* npm run demo:record
*/

/** Long enough to read what changed, short enough not to drag. */
const BEAT = 900;

const OUT = 'recording';
/** The frame is the demo and nothing else; see isolate() below. */
const SIZE = { width: 1120, height: 720 };

/**
* Strip the page down to the demo.
*
* The demo lives well below the fold under a sticky header, so recording the
* page records mostly prose with the interesting part half off screen.
*/
const ISOLATE = `
header, footer, section, .shot-note, .shot-caption { display: none !important; }
body { margin: 0 !important; background: #fff !important; }
.wrap { padding: 0 !important; max-width: none !important; }
.hero { padding: 0 !important; }
.cta, .hero h1, .hero .lede { display: none !important; }
.shot { border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
.demo-main { min-height: 430px !important; }
`;

async function main(): Promise<void> {
await rm(OUT, { recursive: true, force: true });
await mkdir(OUT, { recursive: true });

const browser = await chromium.launch();
const context = await browser.newContext({
viewport: SIZE,
deviceScaleFactor: 2,
recordVideo: { dir: OUT, size: SIZE },
// A cursor would wander about with nothing to explain it.
reducedMotion: 'no-preference',
});
const page = await context.newPage();

await page.goto(process.env.DEMO_URL ?? 'http://127.0.0.1:4173/');
await page.locator('#demo-mount').waitFor({ state: 'visible' });
await page.addStyleTag({ content: ISOLATE });
await page.waitForTimeout(BEAT);

// Kite: small enough that the tree fills the frame rather than being
// scaled down to fit a wide one, and it still finds three shapes.
await page.locator('.demo-graphs .demo-pill', { hasText: 'Kite' }).click();
await page.waitForTimeout(BEAT);

// Watch the search run.
await page.locator('.demo-restart').click();
await page.locator('.demo-play').click();
// Let it run to the end on its own; the button returns to Play there.
await page.locator('.demo-play').filter({ hasText: 'Play' })
.waitFor({ timeout: 30_000 })
.catch(() => undefined);
await page.waitForTimeout(BEAT);

// The shapes, and where they are.
await page.locator('.demo-shape').first().click();
await page.waitForTimeout(BEAT * 1.4);
await page.locator('.demo-instance').nth(1).click();
await page.waitForTimeout(BEAT * 1.6);

await context.close();
await browser.close();

const [recorded] = (await readdir(OUT)).filter((name) => name.endsWith('.webm'));
if (!recorded) {
throw new Error('playwright recorded nothing');
}
await rename(join(OUT, recorded), join(OUT, 'demo.webm'));
console.log(`recorded ${join(OUT, 'demo.webm')}`);
}

main().catch((failure) => {
console.error(failure);
process.exit(1);
});
Loading
Loading