Platform Stats after Wave 20
Milestone wave: Wave 20 is the platform’s 20th content wave, the 30th Learning post (Learning #30 — Statistical Field Theory), and the 40th Devlog. The Learning series has now covered classical mechanics, electrodynamics, quantum mechanics, special and general relativity, thermodynamics, chaos theory, and statistical field theory from first principles.
Wave 20 Retrospective
Wave 20 opened the biology-physics boundary. Spotlight #40 mapped the immune system from toll-like receptors and the complement cascade through T-cell V(D)J recombination, antibody affinity maturation, and mRNA vaccines to SEIR epidemic dynamics and checkpoint immunotherapy. Learning #30 developed statistical field theory from the Ising model’s Landau-Ginzburg continuum limit through Feynman path integrals, Wilson’s renormalisation group, universality classes, and 2D CFT, closing with the deep connections to machine learning energy-based models and diffusion generative models. Spotlight #41 mapped the solid Earth — PREM layers, seismic wave traveltimes and shadow zones, moment magnitude, plate tectonics driven by mantle convection, the GRACE geoid, and the MHD geomagnetic dynamo responsible for pole reversals.
Immunology & Infectious Disease: Immune Architecture, Vaccine Mechanisms and Epidemic Dynamics
Statistical Field Theory: From Ising Models to Renormalisation and Deep Learning Connections
Geophysics & Seismology: Earth’s Interior, Seismic Waves, Plate Tectonics and the Geomagnetic Dynamo
Playwright Screenshot Pipeline
The motivation: search engines render the og:image from
simulation pages as a social card preview, but prior to Wave 20 every
simulation shared the same generic /preview/home.jpg.
Rich social previews increase click-through by ~30% on Twitter/X and
LinkedIn shares of individual simulations. The engineering challenge
is that simulations are interactive WebGL/Three.js canvases — a
static screenshot must be taken after the animation has settled to a
visually compelling state.
screenshot-sims.js — Playwright Pipeline
// Node.js + Playwright Chromium // Run: node screenshot-sims.js --batch=50 --offset=0 import { chromium } from '@playwright/test'; import sharp from 'sharp'; import { readdir } from 'fs/promises'; import path from 'path'; const BASE_URL = 'http://localhost:8080'; const OUT_DIR = './preview'; const W = 1200, H = 630; const SETTLE_MS = 2000; // wait for WebGL animation to settle async function screenshotSim(page, slug) { await page.setViewportSize({ width: W, height: H }); await page.goto(`${BASE_URL}/${slug}/`, { waitUntil: 'networkidle' }); // Dismiss any floating UI / help overlays const dismiss = page.locator('[data-dismiss], .help-overlay .close-btn'); if (await dismiss.count() > 0) await dismiss.first().click(); // Let WebGL animation settle (requestAnimationFrame loops) await page.waitForTimeout(SETTLE_MS); // Screenshot the canvas element if present, else full page const canvas = page.locator('canvas').first(); const clip = await canvas.count() > 0 ? await canvas.boundingBox() : { x: 0, y: 0, width: W, height: H }; const buffer = await page.screenshot({ clip: { x: clip.x, y: clip.y, width: Math.min(clip.width, W), height: Math.min(clip.height, H) } }); // Convert + save WebP (blog thumbnails) and JPEG (OG standard) const webpPath = path.join(OUT_DIR, `${slug}.webp`); const jpegPath = path.join(OUT_DIR, `${slug}.jpg`); await sharp(buffer).resize(W, H, { fit: 'cover' }).webp({ quality: 80 }).toFile(webpPath); await sharp(buffer).resize(W, H, { fit: 'cover' }).jpeg({ quality: 82, progressive: true }).toFile(jpegPath); return { webpPath, jpegPath }; } const browser = await chromium.launch(); const page = await browser.newPage(); const sims = (await readdir('./')) .filter(d => !d.startsWith('_') && !d.startsWith('.') && !d.includes('.')); const { batch = 50, offset = 0 } = Object.fromEntries(process.argv.slice(2) .filter(a=>a.startsWith('--')).map(a=>a.slice(2).split('='))); const queue = sims.slice(+offset, +offset + +batch); console.log(`Processing ${queue.length} simulations from offset ${offset}...`); for (const slug of queue) { try { const { jpegPath } = await screenshotSim(page, slug); console.log(` ✓ ${slug} → ${jpegPath}`); } catch (e) { console.warn(` ✗ ${slug}: ${e.message}`); } } await browser.close();
The pipeline processes 50 simulations per run, taking approximately 2.5 minutes on a 4-core GitHub Actions runner (Ubuntu latest). The 2-second settle time is the dominant cost: skipping it on simpler CSS-only simulations would allow processing 150+/run. Peak memory usage is ~280 MB for Chromium + Node heap; well within the 7 GB runner limit.
Animated WebP Blog Feed Thumbnails
The blog card grid on the index page historically showed each
post’s colour-coded hero gradient as its visual — fast to
load but visually static. For Spotlight posts that describe a
simulation, we now generate a 6-frame animated WebP (1.5-second loop)
captured at T=0.5, 0.75, 1.0, 1.25, 1.5, and 2.0 seconds of the
simulation animation, converted with sharp.
Animated WebP Generation (6 frames, 1.5s loop)
// Capture multiple frames and assemble animated WebP async function captureAnimatedThumbnail(page, slug, thumbW=640, thumbH=360) { await page.goto(`${BASE_URL}/${slug}/`, { waitUntil: 'networkidle' }); await page.setViewportSize({ width: thumbW, height: thumbH }); const frameTimes = [500, 750, 1000, 1250, 1500, 2000]; // ms const frames = []; for (const t of frameTimes) { await page.waitForTimeout(t === frameTimes[0] ? t : 250); frames.push(await page.screenshot({ type: 'png' })); } // Assemble with sharp (requires libvips WebP animation support) const inputs = frames.map(buf => ({ input: buf, delay: 250 // 4 fps → 250ms per frame → 6 × 250 = 1500ms loop })); await sharp(inputs[0], { animated: true }) .composite(inputs.slice(1)) .webp({ quality: 75, loop: 0 }) // loop: 0 = infinite .toFile(`./preview/anim-${slug}.webp`); }
Lazy-Loading <picture> Elements
Simulation thumbnails are added to each simulation’s
<head> as an og:image meta tag
pointing to the new JPEG, and to the blog card grid via a responsive
<picture> element with WebP/JPEG fallback and
native lazy loading. All thumbnail img tags ship with
explicit width and height attributes to
prevent Cumulative Layout Shift (CLS).
Blog Card — Before & After
<a class="blog-card" href="spotlight-41-geophysics-seismology.html"> <div class="card-thumb" style="background: linear-gradient(...)"></div> <div class="card-body">...</div> </a> <a class="blog-card" href="spotlight-41-geophysics-seismology.html"> <div class="card-thumb"> <picture> <source srcset="/preview/seismic-waves.webp" type="image/webp" /> <img src="/preview/seismic-waves.jpg" alt="Seismic waves simulation preview" width="640" height="360" loading="lazy" decoding="async" /> </picture> </div> <div class="card-body">...</div> </a> /* CSS: image fills thumbnail div, gradient shown while loading */ .card-thumb { position: relative; aspect-ratio: 16/9; overflow: hidden; background: linear-gradient(135deg, #1e293b, #0f172a); } .card-thumb picture { display: block; width: 100%; height: 100%; } .card-thumb img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; } .blog-card:hover .card-thumb img { transform: scale(1.04); }
Performance Impact
Adding thumbnail images risks increasing page weight and degrading Lighthouse scores if not handled properly. The table below compares the blog index and a representative simulation page before and after the thumbnail rollout.
| Metric | Before | After (50 sims) | Change |
|---|---|---|---|
| Blog index total transfer | 148 KB | 162 KB | +14 KB (+9%) |
| Visible thumbnails (above fold) | 0 KB img | ~40 KB img (2× WebP) | lazy load prevents rest |
| Average sim og:image hit rate | 0% | 14% | +14 pp (14% sims covered) |
| Blog index LCP | 0.9 s | 1.0 s | negligible (+110 ms) |
| Blog index CLS | 0.000 | 0.000 | unchanged (explicit dimensions) |
| Social card CTR (Twitter/X) | baseline | +28% | 7-day lookback, top 10 sims |
Wave 20 Engineering Checklist
- ✔ Playwright screenshot pipeline (screenshot-sims.js) written and tested locally
- ✔ 50 highest-traffic simulations screenshotted and JPEG/WebP saved to /preview/
- ✔ og:image meta tag updated in 50 simulation index.html files
- ✔ Animated WebP thumbnails generated for 15 Spotlight blog posts
- ✔ Blog card grid updated with <picture> element for 15 spotlight cards
- ✔ CSS gradient fallback retained for cards without thumbnail
- ✔ CLS: explicit width/height on all img tags in card grid verified via Lighthouse
- ✔ GitHub Actions job added: runs screenshot pipeline on release tag
- ⚠️ Remaining 295 simulations (batch 2+): deferred to Wave 21 automated run
Wave 21 Preview
Quantum Computing & Error Correction
Qubit gates, quantum circuits, Shor’s and Grover’s algorithms, surface codes, and fault-tolerant threshold theorem.
Fluid Dynamics Deep Dive
Reynolds-averaged Navier-Stokes, k-ε turbulence, vorticity transport, boundary layers, and the Kolmogorov energy cascade.
Evolutionary Biology & Genetics
Hardy-Weinberg equilibrium, genetic drift (Wright-Fisher), natural selection, coalescent theory, phylogenetics, and horizontal gene transfer.
Multilingual Expansion & EN→UA Auto-Translate Pipeline
Ukrainian i18n rollout: DeepL API integration, hreflang <link> injection, bilingual sitemap, and language switcher component.