What every file in img/ and the section img/ folders is for, the per-use pixel budget every image is encoded to, and the recipes. Read this before adding any image, video or model.
| file | status |
|---|---|
img/intro-bg.webm |
used — first <source> for the People backdrop |
img/intro-bg.mp4 |
used — fallback <source> (Safari / no VP9) |
img/intro-bg.jpg |
used — poster, and the still under reduced motion |
information/img/sussex-bg.jpg |
used — the Information section backdrop, behind all three tabs. 1600×800 / 306 KB. It lived in img/ at the root until that was noticed: img/ is for what more than one section uses, and nothing else has ever asked for this one |
information/img/sussex_landscape.jpg |
used — the campus, in the University block on the Contact tab. 1200×675 / 166 KB from a 2048px original, which is kept as a gitignored _full. Shown at ~320px in a column and ~775px as a banner below 900px, so 1200 is the 2× case that actually happens |
information/img/logo_sussex.svg |
used — the crest, badged on that photograph. 9 KB of line art with no fill of its own, so it renders black and needs the cream plate it is given |
img/og-card.jpg |
used — the social card, 1200×630 / 19 KB. lab_banner_white.png centred on the hero's black at 62% width. Referenced as an absolute URL — no social crawler resolves a relative one |
img/favicon.png, img/apple-touch-icon.png |
used — 48px / 180px, 1.5 and 5.9 KB. lab_logo_black.png cropped to getbbox(), then its lower 16% dropped to lose the Sussex wordmark, on the site's cream so the mark survives a dark tab bar |
img/brain.glb |
used — the hero's 3D brain, 5.7 MB. Repacked from Sketchfab's 13.2 MB (see the repack note at the foot of this file); CC-BY-4.0, dgallichan |
img/magritte_falsemirror.jpg |
used — the eye the Research zoom dives into (2000×1345; the pupil constants are measured off it) |
research/img/train_ciotat_loop.webp |
used — the Lumière brothers' L'Arrivée d'un train, behind the zoom's opening landmark. Animated WebP, 687 KB / 540 × 360 / 143 frames at 15fps, cut to the arrival alone (t = 3.8 → 13.6 of the source). It is a background-image, not a <video> — see research-zoom.md, "The opening landmark", for why that was the fix rather than a preference. Regenerate from train_ciotat.webm with -vf "fps=15,scale=540:-1:flags=lanczos,hqdn3d=5:5:8:8" -c:v libwebp_anim -q:v 50 -compression_level 6 -loop 0 -an. Do not swap it for a GIF: the same cut at 360px, 10fps and 32 colours is 3 MB, because film grain defeats frame-delta compression. Not fetched before the gate opens, and never fetched at all below 900px or under reduced motion — measured, 0 requests |
research/img/train_ciotat_still.jpg |
used — one frame of the same clip (t = 11.2, the locomotive in frame), 13 KB / 540 × 360. What the stacked branch and reduced motion get in place of the loop, chosen in the stylesheet by the query that sets --rz-mode: stack. That branch used to get no film at all |
research/img/train_ciotat.webm, .mp4 |
source only — 2.1 MB VP9/Opus and 1.35 MB H.264/AAC, both 540 × 360 / 51.7s, the full reel. Nothing on the page requests either any more; they are what the WebP and the still are cut from, and the 5.33 MB VP8 original is beside them as a gitignored train_ciotat_full.webm. Untracked, so deleting them is not recoverable from git — by this file's own rule they should either go or become _full-suffixed and gitignored, and that is a call for whoever next touches this |
research/img/era-galvanoscope.jpg |
used — Galvani's bimetallic-arc frog-leg experiment, the deck's fifth card. An anonymous 19th-century textbook plate on cream paper, so it needed the same dark remap as era-wundt once did: mean luminance 222.8 → 102.5, into the paintings' 72–111 band. Cropped from a portrait _full (864 × 1024) to 4:3 at 900 × 675, dropping the French figure caption, which is illegible at card size anyway |
research/img/art_fake.jpg |
used — the AI-Beliefs pair, left card. 737×900, ~95 KB |
research/img/art_real.jpg |
used — same pair, right card. 717×900, ~125 KB |
research/img/era-*.jpg |
used — all seven cards of the Metascience deck, ~810 KB between them, in deck order: era-folly (Bosch), era-pinel (Robert-Fleury), era-phrenology, era-charcot (Brouillet), era-galvanoscope, era-wundt, era-lab. All 900 × 675 / q82 except era-wundt, which is 684 × 513 — the whole of what its source had, and never upscale: the budget is a ceiling. Each is beside its own gitignored _full, cropped to exactly 4:3 so nothing is squeezed — and the card's ratio is 4/3 because of that, not the other way round: changing it would cover these and quietly cut seven chosen crops. era-wundt is the Leipzig laboratory with Wundt in it, which replaced a trade engraving of a bare chronoscope — the room was always what the station claimed. era-lab is the lab's own multimodal session, the one card that is not historical and the one the run ends on, which is the point of it. Every card has a picture, so the ERA_ARTS drawings are a fallback nothing takes — see research-zoom.md |
research/img/copernicus.jpg |
unused — was the Inventions plate until that row went light; nothing requests it now. 1600×1151, 220 KB, and its 4000×2877 / 2.3 MB original is beside it as a gitignored _full, so the web copy is regenerable and safe to delete. Still committed, which is the one thing here that breaks the "nothing no page requests is in the repository" rule |
publications/*/*.pdf |
used — 40 papers, 58.9 MB, copied byte for byte (no ghostscript/qpdf/pikepdf here). Fetched only when a reader presses the badge, so this is clone weight, not page weight |
publications/*/featured.{jpg,png} |
used — 41 figures, 2.3 MB. 39 imported from the old site's content/publication/*/featured.* by tools/import_publication_assets.py, 19.9 MB → 2.1 MB. See the recipe below |
research/img/*_full.jpg, *_full.png |
source only — gitignored, kept on disk so a web copy can be regenerated. Every logo-*.png has one |
research/img/logo-*.png |
used — the Creations cards' pictures, one per tool. Eleven files, 1.07 MB between them (15–188 KB each), each beside its own gitignored _full. They were the _fulls: 9.2 MB, logo-neuropsyxart.png alone 4500×4500 / 5.7 MB. See the recipe below |
news/2025-ai-faces-in-the-news/featured.webp |
used — copied from the old site unchanged, 25 KB |
news/2025-interoception-questionnaires/featured.jpg |
used — re-encoded from the old site's 2.2 MB featured.png at 1400px / q82 |
news/2025-interoception-questionnaires/mint.jpg |
used — same, from a 679 KB PNG at 1100px |
news/2025-interoception-questionnaires/ans.webp |
used — copied unchanged, already 118 KB |
news/2023-new-logo/matrix-*.gif |
used — 2.4 / 4.4 MB originals cut to ~700 KB: 360px wide, every third frame, 64 colours |
news/2023-new-logo/*.png, TheDoors.jpg |
used — copied unchanged, 8–125 KB each |
news/2026-cognitive-elegance/rubin-clip.gif |
used — the Rubin clip, in the body at width: 30%, floated right. 2.7 MB original at 965 KB: 240px, every fourth frame, 48 colours. It is not called featured.gif any more: a still now serves as the hero, and with both files present the hero would have been whichever extension FEATURED_EXTENSIONS happens to list first |
news/2026-cognitive-elegance/featured.jpg |
used — a still of the same clip, two stacked frames, so 4:5 and unusable as a hero raw: a 16:9 card crop shows only the band where the frames meet. The still is centred at 336×420 on a 1000×563 plate of #e9f2ee instead, sized to clear the hero's crop — its max-height: 22rem starts biting above a panel width of ~626px and shows only the middle ~77% of the height at the full 920px, so the still is 75% of the plate's height and never reaches the cut. The crop is vertical only, at every width. The 400×500 original is beside it as a gitignored featured_full.jpg |
news/2023-chatgpt-personality/featured.jpg |
used — re-encoded from a 2.8 MB PNG at 1400px |
news/2020-what-is-reality-bending/*.jpg |
used — hero, don-quixote and wizard, all re-encoded from 0.3–1.5 MB originals |
news/2026-event-triggers/featured.png |
used — copied unchanged, 100 KB |
memories/img/2026_ESCAN_Symposium.webp |
used — the symposium group photo again, 1400px on the long side / 131 KB, a second copy of the post's speakers.jpg because a memory's file has to live in this folder to be found |
news/2026-escan-symposium-artificial-emotion/featured.jpg, speakers.jpg |
used — the symposium's title slide, kept at its native 786px (upscaling a slide export only adds bytes) at 72 KB, and the group photo re-encoded from a 1206×1579 JPEG to 1100px on the long side, 139 KB |
news/2026-skeptic-agnostic-believer/featured.gif |
used — drawn, not photographed: make_figure.py beside it renders the frames with matplotlib and assembles the GIF with Pillow on one 48-colour palette. 1400×560, ~130 frames, ~460 KB — under the GIF budget despite the full hero width, because flat colour on cream compresses in a way a photographic GIF never does. Edit the script and re-run it rather than editing the image |
news/2026-rolling-the-bones/featured.jpg |
used — the shaman reading burnt bones, and the source is square (526×526), which neither crop survives raw: the hero keeps only the middle ~77% of the height and would take the head and the bones with it. Centred instead at its native 526px on a 1248×702 plate of the photograph's own border black #171717 — not the cream the other plated heroes use, because here the plate abuts a dark picture rather than the panel, and at that colour the seam is invisible. 75% of the plate height, so the subject clears the hero crop with ~9px to spare at the worst case (a 920px panel). 52 KB. JPEG and not WebP because this file is also the og:image, and not every social crawler renders a WebP or an AVIF card |
news/2026-rolling-the-bones/hunters.webp, minima.webp |
used — the deer hunt and the local/global minimum surface, both floated right at width: 35% of the 40rem prose column, so ~224px shown and 480px encoded. 45 and 25 KB. A float inside .news-prose only wraps the paragraph it is in — that sheet is display: grid, and a float cannot escape its own grid item, so each of these sits inside a <p> at least as tall as the picture. minima was moved off the two-line paragraph that introduces it for exactly that reason: it left a 115px gap instead of wrapping |
news/2026-rolling-the-bones/attractors.webp |
used — the two attractors and the trajectory crossing between them, 1100×600 / 98 KB. A full-width <figure> rather than a third float, because it carries the post's one figcaption |
news/2024-most-cited-researcher/rg2025_september_november.png, rg2026_may_august.png |
used — the ResearchGate notices the post is about, at their native 598px, shown at max-width: 60% of the prose column (~384px, so still 1.5×). Quantised to a 64-colour palette on the way in: flat UI screenshots, so 122 and 115 KB of RGBA came down to 27 KB each with nothing visibly changed |
news/2024-rob-dickinson-impact-award/featured.jpg |
used — Rob's own people/rob-dickinson/avatar.png, which is a circle on transparency: dropped straight in, a 16:9 hero crop would have sliced the face and shown four transparent wedges. Composited instead at 1000×563 on #e9f2ee — the section's cream under the tint .news-article__hero paints behind a picture that does not fill it. The circle is 420px, not larger, because the reader's hero runs to ~2.3:1 and only the middle ~77% of a 16:9 plate survives that crop; a fuller medallion came out as a capsule. 1000px rather than 1400 because the avatar is 576px and upscaling only adds bytes |
news/2025-eva-mala-socobio-internship/featured.png |
used — the SoCoBio DTP mark on its own dark ground, 739×415 / 23 KB, as supplied. Its wordmark's lower line sits just inside the hero's crop; a taller plate would be safer if it is ever replaced |
news/2025-sam-marine-nhs-prize/featured.png |
used — the NHS wordmark, white on NHS Blue #005eb8 at 1400×788 / 36 KB. Rendered from Wikimedia Commons' National_Health_Service_(England)_logo.svg (public domain as simple text and shapes; the mark itself is a UK trademark, so this is editorial use — see NOTICE.md). There is no rasteriser or Node on the machine this was built on, so the three glyph paths were flattened with a throwaway Pillow script rather than converted; the SVG is 1.2 KB and re-renderable from the same source if the plate ever needs another size. There is a rasteriser on this machine after all: headless Chrome. Size html,body to the plate in an HTML file, inline the SVG, then chrome.exe --headless --disable-gpu --hide-scrollbars --force-device-scale-factor=1 --window-size=1400,788 --screenshot=out.png file:///…/plate.html. Cheaper than flattening paths by hand, and how the first hero for 2024-open-science-sustainability-debate was made, before the BNA's own graphic replaced it |
news/2024-open-science-sustainability-debate/featured.jpg |
used — the BNA's own announcement graphic for the debate, and the source is square (1080×1080), which survives neither crop raw: the hero keeps the middle 77.4% of the height and the card takes 16:9, which of a square is 56%. Centred instead at 591px on a 1400×788 plate of the section cream #e9f2ee — 75% of the plate height, the proportion 2026-rolling-the-bones already uses, which leaves the poster's outer edge ~5px inside the hero's cut at the worst case (an 808px hero box, measured) and its cream card ~22px inside. Cream and not the graphic's own border red: a flat red against the source's textured red drew a visible seam, and the poster's red frame gives it a crisp edge on cream anyway. q85 at 4:4:4 / 91 KB — 4:2:0 saves 25 KB and softens the green and red text, which is most of the picture. The 1080 original is beside it as a gitignored featured_full.jpg |
news/2026-university-after-ai/featured.gif |
used — the debating-society photograph with the post's title bleeding down it, 600×394 / 244 KB, 23 frames on a 2.8s loop, built by make_hero.py beside it. 600px is the whole of what the source had, and the budget is a ceiling, so it is not upscaled. There is no melted-horror face on this machine and no licence to ship one, so the melt is procedural — the title is set in Impact, its contour displaced by two smooth noise fields, and each drip grown downward off the bottom contour to a terminal bead, all at 4x and downsampled, which is what stops a roughened edge aliasing into fringes. Three things this file is the record of: a drip is a spec, not a drawing, so one number per frame (grow) is the whole animation and the poster is simply one value of it; the dark ground is painted once and held, because recomputing a 17px-blurred halo per frame changes almost every pixel around the title on every one of them and cost 654 KB against 366 KB; and 64 colours, not 255, which on this picture is not tellable apart at 4x because the film grain dithers it already, and saves a further 122 KB. The title lives in the middle 58% of the height, because that is all the reader's hero keeps of a 1.52:1 picture at a 920px panel — measured in the browser, not assumed; the script prints the clearance for both crops on every run (+70px card, +16px hero). --still writes the same frame as a JPEG if a still is ever wanted back, but only one featured.* may exist, or the hero becomes whichever extension FEATURED_EXTENSIONS lists first. The source photograph is beside it as a gitignored featured_full.jpg — its provenance is unestablished, so settle that before the post leaves draft |
news/2026-evidence-accumulation-widget/featured.gif |
used — cogmod's widget capture with a spinning cut-out reaction face, a red arrow and the cogmod hex on it, built by make_thumbnail.py beside it. It takes its three sources as arguments — a path or an http(s) link it caches locally — so only reactface.mp4 is committed, being the one input no other repository has; the capture (cogmod/man/figures/anim_widget.gif) and the logo come from the cogmod checkout, the capture cached as a gitignored featured_full.gif. 820×461 / 134 frames / 1.9 MB, and that last number is the thing to read before touching this file. Four things it is the record of. The capture is cropped to the card's own 16:9 first, which is what makes a corner of the file a corner of the picture anyone sees: uncropped, the reader's hero keeps only the middle 52% of the height and the card 67%, so a logo in the true top-right corner is in neither. Cropped, the card shows the whole file and the hero still keeps 78% — and nothing is lost that the site ever showed, the crop dropping the title and model tabs off the top and the assumptions row off the bottom. A spinning sticker is bounded by its diagonal, so the face is scaled from the sweep radius, not the other way round. A constant spin costs about three times the file, and there is no way round it: a large photographic region changing on every frame is the one thing a GIF cannot encode cheaply — 6.4 MB at full frame rate, 1.9 MB after every lever below, against 709 KB for the same composition with the spin as a 1.6s burst, which is what this was before someone asked for the constant one. The levers, measured: FRAME_STEP = 2 (every 2nd frame at twice the delay, same 26.8s) halves it outright; dropping the sticker's drop shadow is worth 26% (2921 KB → 2167 KB), because a soft ramp around the whole perimeter re-quantises differently on every frame where on a still sticker it is nearly free; and the palette is down to 48. dither=Image.NONE throughout, worth 821 KB against 530 KB when this was still. The face passes behind the logo by draw order rather than by geometry. --still writes a gitignored poster with the hero band and the sweep circle drawn on it |
news/2026-evidence-accumulation-widget/logo.webp |
used — the same cogmod hex again, in the body floated right at width: 20% of the prose column. 260×300 for a 128px display, and WebP not PNG: 14 KB against 80 KB for the identical picture with its alpha intact — flat neon on a dark ground is exactly what lossy-with-alpha is good at. The paragraph it floats in measures 162px against the picture's 148px, which is the margin the whole float depends on: .news-prose is a grid, a float cannot escape its own grid item, and a picture taller than its paragraph leaves a column-wide gap rather than wrapping — see news.md |
news/2023-ohbm-brainart/featured.jpg |
used — the exhibition stand at OHBM 2023, cropped to the card's own 16:9 out of memories/img/2023_dom_ohbm.jpg. That memory photo is the source of record (2000×1500, already in the repository), so there is no _full beside this one — re-crop from the memory if it ever needs another size. 1400×788 / 161 KB. The crop is y 0–1125 of the source and not centred on the frame: the reader's hero keeps only the middle 77.4% of a 16:9 picture at a 920px panel (measured, not assumed), and the subject band runs from the top of Dom's head to the foot of the two canvases — so the crop centres that, which leaves +34px above the head and +41px below the canvases. Cropped to 16:9 first, the card shows the whole file. JPEG and not WebP because this file is also the og:image |
news/2023-ohbm-brainart/attractor.webp |
used — the same EEG segment drawn twice, as a time series and as a state-space trajectory, lifted from the proposal that was submitted to the BrainArt SIG. 1100×457 / 23 KB, a full-width <figure> in the prose column. WebP against 40 KB as JPEG and 41 KB as a 64-colour PNG — flat line art on white is the case where the alternatives both lose |
news/2023-ohbm-brainart/catalogue.webp |
used — the 2023 Brain Art Exhibition's own catalogue page, cropped to the whole of the lab's entry: the banner, the artist block, the statement, the two attractors and the explanatory panel, down to the rule that closes it before the next artist. It is in the post as evidence rather than as reading matter — an outside page listing the work carries what a paragraph paraphrasing the artist's own pitch cannot. 1100×1367 / 99 KB, full width in the prose column, so ~795px tall on screen: tall on purpose, and the one figure here that is not sized to be read. Captured with headless Edge, not the preview pane, which caps a screenshot at its own width (~800px): msedge.exe --headless=new --hide-scrollbars --force-device-scale-factor=2 --window-size=1440,2600 --virtual-time-budget=12000 --screenshot=<abs path>, then cropped and downsampled to 1100 — 2× then down is what keeps the small type crisp. Four things it is the record of. --screenshot needs an absolute path, or it fails with "Access is denied" and writes nothing. Google's cookie notice is position: fixed at the bottom-left of the viewport, so the window is captured far taller than the crop needs and the notice lands below it — nothing had to be clicked or dismissed, and a window sized to the crop would have put the notice inside it. The cut is measured, not eyeballed: an artist's block is a full-width band of the site's dark green, so the next artist is found by scanning rows for that colour and the crop lands in the gap above it. And the video embed's right edge is clipped in the picture because the page itself clips it — that is what a visitor sees, so it was left alone. Re-shoot if the page changes; there is nothing here to regenerate it from |
news/2023-ohbm-brainart/sketch.webp |
used — the pencil sketch of the display proposal, from the same submission. Shown at max-width: 55% of the prose column (352px), so 700×782 encoded / 46 KB, against 78 KB as JPEG. A centred <figure> and not a float: it is portrait, and a float only wraps the paragraph it sits in — no paragraph in this post is the 393px tall it would need. The originals of both figures live outside the repository, in the OHBM submission, so neither is regenerable from anything here |
news/2024-breaking-barriers-reproducibility/featured.jpg |
used — a screenshot of the Sussex page the post links to, taken by make_hero.py beside it, which is the only thing that can regenerate it: re-run it if that page is restyled. 1400×788 / 132 KB, q88 at 4:4:4, because most of the picture is small text and the Sussex green. Playwright at a 1600px viewport and device_scale_factor=2, then down — 2× then down is what keeps the type crisp, the same recipe 2023-ohbm-brainart/catalogue.webp records for headless Edge. Three things it is the record of. The cookie banner is hidden from the outside (#cookie-options { display: none }), not answered: it is the first element on the page and pushes the layout down ~320px, and hiding it records no consent either way. The cut is at the photograph's bottom edge, one line above the page's own caption, which misspells the name. And the capture is 988×560 centred on a cream plate rather than filling it: the reader's hero is 823px wide at every viewport width from 1280 up (measured — the max-height: 22rem is what pins it), so it keeps 76% of the height and cuts 95px off each end, which would take the Sussex header and the photograph with it. The plate's 114px band clears that by 19px. JPEG and not WebP because this file is also the og:image |
the fifteen posts ported from content/post/ |
used — every still re-encoded on the way in (1400px hero, 1100px in-body, q82); heroes land at 20–300 KB against originals up to 3.4 MB. GIFs copied unchanged where already under ~700 KB |
img/Intro.mp4 |
source only, 54 MB — gitignored, and no longer in the repository. encode_intro_bg.sh takes its source as $1 if you bring it back |
The two paintings are both by people, and which is which matters: art_fake is
the one viewers reliably call AI, art_real the one they call human. Swapping
the filenames silently inverts the demonstration. Regenerate the web copies from
the _full originals at 900px on the long side, quality 82. Both are portrait
at very close to 4:5, which is what .rz-artworks__frame's aspect-ratio is
set to — replacing one with a differently shaped canvas means revisiting that
number, or the pair stops reading as one comparison.
Every image on the site is encoded to the largest size it is ever displayed at, doubled for a 2× screen — not to whatever came off the camera:
| budget | because | |
|---|---|---|
| avatar | 700px | 188px in the profile panel, 156px in the People grid |
| memory photo | 1400px | the lightbox is min(90vw, 960px) |
| memory GIF | 480px wide | same, and see the GIF recipe below |
| post hero | 1400px | the reader panel is min(920px, 100vw) |
| post figure | 1100px | inside that panel's own column |
| section backdrop | 1600px | full-bleed |
| publication figure | 1000px | a 13rem column |
| collaborator | 400px | a small round portrait |
| tool logo | short side ~1000px | a 233px Creations card — generous on purpose, these are the pictures the row is made of |
| timeline plate | 900px wide | a 416px 4:3 frame at its widest, cropped to that ratio on the way in |
This is written down because the site drifted a very long way from it while these notes were being careful about news images. Measured on a cold load the page pulled 122 MB, 106 MB of it images: a 20 MB GIF, a 14 MB avatar for a 156px thumbnail, three memory GIFs totalling 52 MB. Re-encoding 22 files to the table above took 99 MB down to 10.8 MB and nothing on screen changed.
Two things that were doing most of the damage, and are the first things to check on any new asset:
- A photograph saved as PNG. Of the eleven PNGs over 400 KB here, exactly
one used its alpha channel. The rest were JPEGs waiting to happen — the
14 MB avatar became 80 KB. Test the channel (
getchannel("A").getextrema()), do not assume from the extension; and when converting, the filename changes, so re-run the manifest script that resolves it (update_people.py,update_publications.py) or fix the hand-kept manifest (collaborations_manifest.json) and content module by hand. - A GIF nobody re-encoded. The recipe is in the table above: width down
first, every Nth frame with the swallowed delay added to the frame kept, and
a 64-colour palette. Two PIL details cost real time — quantise every frame
against one shared palette, or the encoder dies on "Transparency for P
mode should be bytes or int" after truncating the file it was writing; and
write to a temporary file and
replace(), havingclose()d the source, or Windows refuses the rename and you are left with the zero-byte GIF.
It was the largest single asset and ~80% of a cold load. It needed no mesh tool, no npm and no Blender, because most of its size was how Sketchfab wrote it rather than what it contains — it is pure geometry, no textures, one material, 215,601 verts / 377,701 tris. Three things, in order of size:
- COLOR_0 was 3.45 MB the site never reads.
makeBrainMaterial()throws the glTF's own material away and builds aMeshStandardMaterialwithvertexColorsunset, so the per-vertex colours were decoded, uploaded to the GPU and ignored on every visit. Dropping the attribute is not lossy; it is dead data. If the brain ever needs per-region vertex colour, this is the thing that was removed — re-export from the source model rather than looking for it in the file. - The indices were u32 for meshes that already fit u16. Sketchfab split the brain into primitives of exactly 65,532 vertices — the u16 limit — and then wrote 32-bit indices anyway. Halving them is lossless. Any replacement model is worth checking for the same thing.
- NORMAL was f32, now i8 normalized under
KHR_mesh_quantization, which three.js's GLTFLoader reads natively. Because normals are normalized integers, no node transform changes — which is exactly why POSITION was left alone: quantising it means baking a dequantisation scale into the node hierarchy, and that is where a repack becomes surgery.
Measured against the original through the site's own three.js r167: identical vertex count, triangle count, bounding box and centre, position delta exactly 0, worst normal error 0.376° over 5,830 sampled vertices.
The repack also strips KHR_materials_pbrSpecularGlossiness, which three.js
dropped in r160. sanitizeBrainGlb in brain.js rewrites the GLB's JSON chunk
at runtime to remove it — that is now a no-op on this file and could go, but it
is cheap and it is what makes a fresh Sketchfab export work, so it stays.
Further, if it is ever needed: DracoPy (pip install DracoPy, bundled
wheels, no npm) would reach ~1.5 MB, at the cost of shipping three's
DRACOLoader and a ~200 KB wasm decoder that must be fetched before anything
renders. Decimation below 377k triangles is the only genuinely lossy option and
needs trimesh or open3d.
Large originals are not committed, and .gitignore is what keeps it that way:
*_full.jpg, *_full.png, *_full.webp and img/Intro.mp4. A source
arrives in whatever format it arrives in — the timeline's plates came as a
.webp and a .png among the jpegs, and the _full rule has to cover the
extension or a 10 MB original walks into the repository. They stay on disk so a
derivative can be regenerated — encode_intro_bg.sh still takes its source as
$1 — but nothing that no page requests is in the repository. This had drifted:
Intro.mp4 (54 MB) and every *_full.jpg were tracked, ~62 MB every clone paid
for. img/sussex.png (5.7 MB, superseded by sussex-bg.jpg) was the last one
left and has been deleted.
The same applies to anything carried over from the old Hugo site, which was
never size-conscious about post images — content/post/ has a 3.4 MB PNG and a
2.8 MB GIF sitting in it. Re-encode on the way in and point the post's own
content at the new filename. Nothing enforces this; a post will happily
reference a 3 MB PNG.
- stills: 1400px on the long side for a hero, ~1100px for an in-body figure, quality 82
- tool logos (
research/img/logo-*.png): keep the original as_full, which is gitignored, and write the web copy beside it. Resize so the short side is ~1000px, never upscale, and cap the long side at 1600px — that last one is for the extreme aspect ratios (logo-sequentialsamplingmodels_full.pngis 6000×2196, where the short-side rule alone asks for 2732px of wordmark). Then save an optimised PNG, and only if it lands over ~400 KB, tryim.quantize(colors=256, method=Image.FASTOCTREE)— that is the mode that keeps alpha — and take it if it saves more than 30%. Six of the eleven are palette PNGs on those terms and none of them bands: these are flat-colour marks, and even the generative line drawing (logo-neuropsyxart, 4500×4500 / 5.7 MB → 1000×1000 / 131 KB) survives it. Check the result by eye at full size before keeping it; a quantised gradient is the failure mode to look for. - older logos came from the Hugo site's
featured.pngs. NeuroKit2's already had an alpha channel; Pyllusion's was flat on white and had to be un-composited, not keyed out — a threshold leaves a white fringe on every antialiased edge. For a flat-white background,A = 1 - min(R,G,B)/255andF = (C - (1-A)·255) / Ais exact, and the magenta survives it. Then crop togetbbox()before resizing, or the card's picture is mostly the original's margin. - publication figures imported from the old site
(
tools/import_publication_assets.py): the two trees share no key — old folders are citation keys (makowski2015emotion), new ones are<year>_<FirstFourTitleWords>— so the join is the DOI in the old front matter, falling back to an exact normalised title where they disagree, which 15 of 39 do. Three things that made the first pass wrong and are guarded now: one figure is a.gifand was skipped in silence because the extension was not in the list, giving 37 imports where 38 were due — an unrecognisedfeatured.*is now reported; JPEG is the wrong format for several of these, which are flat-colour diagrams and wordmarks, so both encodings are written and the smaller kept; and a source already inside the budget is copied verbatim, because some went through a PNG optimiser years ago that PIL cannot match and re-encoding turned 33 KB into 70 KB. Do not quantise these — by this file's own rule that is for files over ~400 KB, and the largest here is 159 KB. All 39 match now —nicolas2017centenairedid not until its DOI was added toEXTRA_DOIS, which is what an unmatched line usually means: a paper the manifest does not have rather than a broken join. - ffmpeg is available, and this file said for a long time that it was not.
Not on
PATH, which is where the belief came from — it ships inside theimageio-ffmpegwheel, andpy -c "import imageio_ffmpeg; print(imageio_ffmpeg.get_ffmpeg_exe())"prints the path to a full build (libvpx-vp9, libx264, libopus, aac all present). That is what re-encoded the Ciotat film. Check there before concluding a video job cannot be done here. - animated GIFs: PIL will resize one frame by frame. 360px wide, every third frame and a 64-colour adaptive palette took the two Matrix clips from 2.4 / 4.4 MB to ~700 KB, which is about as far as the format goes. They are only fetched when the post is opened. Check the original's dimensions first. The Cognitive Elegance GIF is 286px wide, and a first pass at "420px, every third frame" made it larger than the 2.7 MB source — upscaling a GIF while re-quantising it is the worst of both. It came out at 965 KB once the width went down rather than up.
Chrome power-pauses media with no audio track — "video-only background media
was paused to save power" — so it starts and then sticks on a frame. The
encoder muxes in anullsrc for a few KB to avoid that classification. Do not
"optimise" it back to -an.
Also: preload="none" leaves the fetch suspended. Raising preload to
"auto" does not reliably restart it and neither does play(); call
video.load() explicitly. And never swallow play() rejections — the console
message is what identifies these.