fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / tools / screenshots.mjs

Branch mistral-medium-35-littlejs · default branch · all branches

screenshots.mjs

5.4 KB · history · blame

1/**
2 * Photograph the built app at phone size into docs/screenshots/, for the README and for
3 * looking a change over before it is committed.
4 *
5 * npm run screenshots
6 */
7import { mkdirSync } from 'node:fs'
8import { chromium } from 'playwright'
9import { preview } from 'vite'
10
11const OUT = 'docs/screenshots'
12const PHONE = { width: 390, height: 844 }
13
14/**
15 * What to photograph: where, and what to do there first. Always lake number 1, and a
16 * time of day by name, so that the pictures are of the same thing from one run to the next.
17 */
18const SHOTS = [
19 { name: 'autopilot', query: '?seed=1&weather=clear&time=day&s=400', act: async () => {} },
20 {
21 name: 'steering',
22 query: '?seed=1&weather=clear&time=day&s=2600',
23 act: async (page) => {
24 await page.keyboard.down('ArrowRight')
25 await page.waitForTimeout(1500)
26 },
27 },
28 // The first fork: the island between its two channels is a little way ahead.
29 { name: 'fork', query: '?seed=1&weather=clear&time=day&s=790', viewport: { width: 844, height: 390 }, act: async () => {} },
30 { name: 'pass', query: '?seed=1&weather=clear&time=day&s=6400', act: async () => {} },
31 { name: 'sea', query: '?seed=1&weather=clear&time=day&s=3400', viewport: { width: 844, height: 390 }, act: async () => {} },
32 // Impossibly thin ice lies to the left of the middle here, faintly lit from within.
33 { name: 'thin-ice', query: '?seed=1&weather=clear&time=day&s=2795', viewport: { width: 844, height: 390 }, act: async () => {} },
34 { name: 'about', query: '?seed=1&weather=clear&time=day&s=400', act: async (page) => { await page.click('#about') } },
35 // A bed of reed islets, just down the lake from where it starts.
36 { name: 'islets', query: '?seed=1&weather=clear&time=day&s=150', viewport: { width: 844, height: 390 }, act: async () => {} },
37 { name: 'islets-dusk', query: '?seed=1&weather=clear&time=dusk&s=7040', act: async () => {} },
38 // A long island of reeds in the middle of the lake: coming up to it, and alongside it.
39 { name: 'island', query: '?seed=1&weather=clear&time=day&s=2880', viewport: { width: 844, height: 390 }, act: async () => {} },
40 { name: 'island-alongside', query: '?seed=1&weather=clear&time=dusk&s=3010', act: async () => {} },
41 // Out at sea, held hard over for a whole circle: the skater comes round onto their own tracks.
42 {
43 name: 'tracks',
44 query: '?seed=1&weather=clear&time=day&s=7880',
45 viewport: { width: 844, height: 390 },
46 act: async (page) => {
47 await page.keyboard.down('ArrowRight')
48 await page.waitForTimeout(12_500)
49 await page.keyboard.up('ArrowRight')
50 },
51 },
52 // At an Olympic pace, a few seconds in: down in a crouch, with a wider view.
53 { name: 'olympic', query: '?seed=1&weather=clear&time=day&pace=olympic&s=5200', viewport: { width: 844, height: 390 }, act: async (page) => { await page.waitForTimeout(9000) } },
54 // The same water at the other times of day.
55 { name: 'dusk', query: '?seed=1&weather=clear&time=dusk&s=5200', viewport: { width: 844, height: 390 }, act: async () => {} },
56 { name: 'night', query: '?seed=1&weather=clear&time=night&s=5200', viewport: { width: 844, height: 390 }, act: async () => {} },
57 { name: 'aurora', query: '?seed=1&weather=clear&time=aurora&s=5200', viewport: { width: 844, height: 390 }, act: async () => {} },
58 { name: 'aurora-phone', query: '?seed=1&weather=clear&time=aurora&s=400', act: async () => {} },
59 { name: 'settings', query: '?seed=1&weather=clear&time=night&s=400', act: async (page) => { await page.click('#settings') } },
60 // Weather, and what is in the sky besides stars. Lake 13 has a comet and lake 2 an aeroplane.
61 { name: 'snow', query: '?seed=1&weather=snow&time=day&s=5200', viewport: { width: 844, height: 390 }, act: async () => {} },
62 { name: 'blizzard', query: '?seed=1&weather=blizzard&time=dusk&s=5200', act: async () => {} },
63 { name: 'comet', query: '?seed=13&weather=clear&time=night&s=3400', viewport: { width: 844, height: 390 }, act: async () => {} },
64 { name: 'aeroplane', query: '?seed=2&weather=clear&time=day&s=3400', viewport: { width: 844, height: 390 }, act: async () => {} },
65 { name: 'landscape', query: '?seed=1&weather=clear&time=day&s=5200', viewport: { width: 844, height: 390 }, act: async () => {} },
66 // Ten thousand kilometres down the lake: the picture must be as sharp as at the start.
67 { name: 'far', query: '?seed=1&weather=clear&time=day&s=10000000', act: async () => {} },
68]
69
70mkdirSync(OUT, { recursive: true })
71const server = await preview({ preview: { port: 4317, strictPort: true } })
72const base = server.resolvedUrls.local[0]
73// The full Chrome for Testing build reaches the GPU; the headless shell draws in software.
74const browser = await chromium.launch({ channel: 'chromium' }).catch(() => chromium.launch())
75try {
76 for (const shot of SHOTS) {
77 const page = await browser.newPage({ viewport: shot.viewport ?? PHONE, deviceScaleFactor: 2 })
78 const errors = []
79 page.on('pageerror', (e) => errors.push(e.message))
80 page.on('console', (m) => m.type() === 'error' && errors.push(m.text()))
81 await page.goto(base + shot.query, { waitUntil: 'networkidle' })
82 await page.waitForTimeout(1500)
83 await shot.act(page)
84 await page.screenshot({ path: `${OUT}/${shot.name}.png` })
85 await page.close()
86 if (errors.length) throw new Error(`${shot.name}: ${errors.join('; ')}`)
87 console.log(` ${OUT}/${shot.name}.png`)
88 }
89} finally {
90 await browser.close()
91 await server.close()
92}