A scroll-led history of technology told through its artefacts, from a struck stone 3.3 million years ago to the machines that are learning to judge.
Live: https://matheuspavaneli.github.io/art/
Technology did not begin with computers, and it did not begin with us. The opening claim of the site is that the first tools predate our genus: the cores and flakes at Lomekwi 3 in West Turkana are dated to about 3.3 million years ago, half a million years before Homo appears at all.
Each of the eight chapters is one artefact, and each externalises a human capacity into an object:
| # | When | Artefact | What was externalised |
|---|---|---|---|
| 01 | ~3.3 Ma | Lomekwi lithic core | the hand |
| 02 | ~400 ka | Controlled fire | heat, and the night |
| 03 | ~10 ka | Grain store and wheel | time |
| 04 | ~3200 BCE | Cuneiform tablet | memory |
| 05 | 1450 | Gutenberg press | the copy |
| 06 | 1769 | Steam engine and dynamo | force |
| 07 | 1947 | Transistor and ARPANET | calculation, and distance |
| 08 | 2012 | Learned machines | judgement itself |
The last chapter is deliberately unresolved.
Deep time is scroll distance. The interval between two eras sets the height of the silence before it, on a logarithmic scale. Two million nine hundred thousand years separate the first worked stone from controlled fire, and that is the tallest stretch of the page. Sixty-five years separate the transistor from learned machines, and it passes in less than a screen. The page accelerates because the history did.
Matter is conserved. There is one cloud of points and it is never created or destroyed, only reassembled. The same particles that were a struck cobble become a rising plume, a wheel, a wedged clay slab, a forme of type, a cylinder and its flywheel, a lattice of conductors, and finally a cluster of weights that never settles. Each era also has a temperament — how its matter behaves at rest.
Colour is position. One accent is live at a time and the page walks the spectrum forward, so the era is legible from the colour alone with the screen out of focus.
- Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4
- three.js with React Three Fiber — one pinned canvas for the whole page
- postprocessing — a bloom pass in its own chunk, desktop only
- GSAP (ScrollTrigger, SplitText) driving Lenis from a single ticker
- node:test with Node's native type stripping — a test gate with no test dependency
Typography is Bricolage Grotesque, Instrument Serif and IBM Plex Mono.
Requires Node 24+ and pnpm.
pnpm install
pnpm dev # http://localhost:3000pnpm test # unit tests
pnpm typecheck
pnpm lint
pnpm buildPushing to main builds a static export and publishes it to GitHub Pages.
Tests, typecheck and lint run first, so a broken commit does not reach the
published site.
The export settings are opt-in behind GITHUB_PAGES=true, so local
development and next start keep the normal server behaviour. A project site
is served from a subpath, so PAGES_BASE_PATH supplies the base path and
asset prefix.
Lighthouse 12, production static export, served with gzip, simulated throttling:
| before | after | |
|---|---|---|
| Performance, desktop | 82 | 100 |
| Performance, mobile | 90 | 99 |
| Accessibility / Best practices / SEO | 100 / 100 / 100 | 100 / 100 / 100 |
| Metric (mobile) | weight | before | after |
|---|---|---|---|
| Total blocking time | 30 | 291 ms | 79 ms |
| Largest contentful paint | 25 | 2.67 s | 1.97 s |
| Cumulative layout shift | 25 | 0.058 | 0 |
| First contentful paint | 10 | 0.77 s | 0.76 s |
| Speed index | 10 | 1.01 s | 0.76 s |
Initial script went from 239 KB to 174 KB gzipped. Those five metrics are the whole score; every other performance item Lighthouse prints carries a weight of zero and is advice about how to move them.
Every number here comes from the same build the site deploys, served locally with gzip and hashed immutable caching, so the report is not measuring a dev server.
One trap is worth naming, because it invalidates the first set of numbers
anyone collects this way: headless Chrome reports
prefers-reduced-motion: reduce by default. Measured that way the page never
splits a line, never starts Lenis and never runs a tween — a page nobody
visits. Every figure above was taken with motion forced on.
Run-to-run variance on a working laptop is real: mobile blocking time moved between 196 ms and 532 ms across repeats of the same build. Decisions were taken on repeated runs, never on one.
Attribution first, from the trace, before touching anything:
- 482 ms of Style & Layout on desktop. Sixty
Revealblocks each built aSplitTextin a mount effect, and splitting text into lines forces a layout read. Sixty forced reads of a 26,000-pixel page, in one hydration pass. - A 404 ms task on the framework chunk, which was React hydrating sixty client components that render nothing.
- three.js in the same window, fetched and parsed while the reader was still trying to read the first screen.
- A layout shift of 0.058 traced to
<main>— the overture veil had no CSS at all. It was falling into the document flow, pushing the whole page down and taking its premise line with it. The intro had never been visible.
Revealstopped being a client component. It renders an inert[data-reveal]mark on the server; one director picks all of them up and splits a block only once it is within a screen of the viewport. Sixty client components became one, and sixty forced layouts became three or four.- The opening beat moved to CSS. The headline, the standfirst and the veil animate from the first painted frame. Waiting for hydration meant the reader watched the headline appear and then be taken away again when the script arrived to animate it — a bug that had been shipping as a feature.
- The heavy layers wait for the reader's first move. three.js (~230 KB gz) and GSAP with Lenis (~55 KB gz) are both scroll-led: neither has anything to do before a scroll, a wheel, a touch, a key or a mouse that moves. The artefact fades up rather than appearing.
- Era progress dropped ScrollTrigger for a passive scroll listener with one rAF coalesce, which takes the animation library off the critical path for the accent and the rail.
- The canvas is tiered. A phone or a low-core machine never downloads the post-processing chunk and gets a third of the points; a software renderer — no working GPU driver — drops further still, because a full-screen blur rasterised on the CPU costs hundreds of milliseconds a frame.
- The overture was rewritten without GSAP, on a rAF tween and CSS keyframes, and given the styles it never had: a fixed layer, out of the flow, so it cannot move the page under the reader.
Each of these was implemented, measured over repeated runs, and reverted:
| Change | Result |
|---|---|
font-display: optional on the display face |
Mobile 100 — LCP collapses to FCP. Declined: a first-time reader on a slow connection would spend the whole visit in a fallback face, and the typography is part of the argument. |
| Dropping the font preloads | First paint 0.76 s → 1.66 s. Worse. |
Inlining the stylesheet (experimental.inlineCss) |
First paint 0.76 s → 0.83 s. Worse. |
| Headline entrance without the fade | LCP unchanged across runs. Reverted, design kept. |
optimizePackageImports for three and gsap |
Byte-identical chunks. Turbopack already does it. |
unused-javascript (53 KB) is React and the Next runtime on a single-route
page. legacy-javascript (13 KB) is transforms inside the framework bundle; a
modern browserslist took it down from 44 KB and the rest is not ours to
remove. render-blocking-resources is one 6 KB stylesheet, and inlining it
measured worse. All three carry a weight of zero.
The one real gap is mobile LCP at 1.97 s. The headline is the largest paint and it repaints when Bricolage Grotesque lands — the four preloaded woff2 files are the dependency, and the only change that removes it is the one that was declined.
The heavy layers load on the reader's first move, and PageSpeed never moves. The gain is real either way — nothing blocks the opening screen — but part of the mobile number comes from the test not doing what a reader does.
Performance work is only worth it if the piece survives it. Nothing above removed a beat, a chapter, a colour or a transition. The entrance the reader sees is the entrance that was designed, moved off a script that arrived too late to run it.
Reduced motion is a contract, not a switch. It removes the idle animation — the drift, the flicker, the rotation — and skips downloading the animation library entirely. It does not remove colour, form, framing or content, because those are the reader's position in the history rather than decoration.
Everything the site argues exists as text. Every chapter is server-rendered, the canvas is decorative and hidden from assistive technology, the accent never carries meaning on its own, and every date is sourced on the page.
52 tests, and none of them touch a DOM. They cover the invariants the piece depends on, which is what breaks silently:
- the interval before each era shrinks monotonically — the page accelerates because the history did, and a wrong threshold height quietly destroys that
- framing is continuous: no scroll step produces a jump — the camera bug that made the artefact change size out of nowhere
- no form collapses to a point, no two eras read as the same cloud — conserved matter has to stay legible as matter
- accentAt mixes strictly between two neighbours at a midpoint — colour is position, so the walk has to stay monotone
They run on node:test with native type stripping: a test gate with no test
dependency.
The pipeline gates the deploy. Pushing to main runs the build, then
tests, typecheck and lint, and only then uploads. The build comes first because
the route types it generates are what the typecheck reads. A failure at any
step means nothing is published.
Dead code is audited, not accumulated. Every export, CSS rule and dependency was checked against what the page actually reaches for: two CSS rules no component has a class for, a scroll lock keyed to a state nothing sets any more, six types exported without a reader, a GSAP plugin registered and never used, and two dependencies for a model pipeline this site does not have.
The opening date follows Harmand et al., 3.3-million-year-old stone tools from Lomekwi 3, West Turkana, Kenya, Nature 521 (2015).
Every quotation is real and attributed on the page with its work and year: Carlyle, Darwin, Rousseau, Plato, Bacon, Marx, Wiener and Turing. The Plato is named as the Jowett translation rather than presented as the only rendering.