Skip to content

markksantos/endless-scroll

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

♾️ Endless Scroll

An infinite generative artwork in a single SVG file — five parallax layers scrolling forever, with no visible loop point

SVG CSS Python License: MIT

Features · Getting Started · The Layers · How It Works · Tuning · Tech Stack


Endless Scroll


Most "infinite" web backgrounds are a tiled PNG dragged across the screen, and you can always find the seam if you stare long enough. Endless Scroll has no images, no JavaScript, and no build step — three glowing threads, a contour field, constellations and drifting dust are generated as pure geometry and scrolled by five CSS keyframes at five different speeds.

The loop is silent by construction, not by crossfade. Every layer is a 1200px tile drawn twice and translated by exactly one tile height, and every piece of content in that tile is built to be periodic in y with period 1200 — the meandering threads are sums of sine harmonics of 2πky/H, so a thread's end joins its own start with matching position and slope. Park a screen recorder on it for an hour and there is no frame where the artwork restarts.

✨ Features

  • Genuinely seamless — the tile boundary is mathematically continuous, not blended. Threads, contour lines and drifting dust all pass straight through the seam mid-frame
  • Five-layer parallax — dust, contours, constellations, threads and ring geometry each scroll one tile height over their own duration (150s → 30s), so depth comes out of timing alone
  • Zero assets, zero JS — one 71 KB .svg file. No textures, no fonts, no libraries, nothing to load. Open it straight from the filesystem
  • Living detail — glowing motes travel along the threads via animateMotion, counter to the scroll, so the threads read as currents instead of wallpaper
  • Composed like a window — a gradient mask fades the top and bottom 9%, a radial vignette closes the corners, and a slow 14s bloom breathes behind everything
  • Responsive by defaultpreserveAspectRatio="xMidYMid slice" makes it cover any viewport, portrait or landscape, without letterboxing
  • Respects prefers-reduced-motion — the scroll stops for anyone who has asked their OS for less animation
  • Regenerablegenerate.py is the source of truth. Change the seed for a completely new composition, or tune the palette, densities and speeds in one place

🚀 Getting Started

1. Clone

git clone https://github.com/markksantos/endless-scroll.git
cd endless-scroll

2. Open it

open endless-scroll.svg        # macOS — any modern browser works

There is no server and no build step. The file is self-contained.

3. Regenerate it (optional)

python3 generate.py

Standard library only — no pip install. It rewrites endless-scroll.svg in place.

Using it in a page

<!-- as a full-bleed background -->
<img src="endless-scroll.svg" alt="" style="position:fixed;inset:0;width:100%;height:100%;object-fit:cover">

CSS animations and SMIL both run inside an <img>, so it animates without being inlined. Inline the markup instead if you want to restyle the layers from the host page.

🌀 The Layers

Layer Duration Content Reads as
dust 150s 300 stars, r 0.4–1.9 Deep background
contour 84s 30 topographic lines, two harmonics each Terrain haze
const 62s 6 constellations, nearest-neighbour linked Mid-distance
thread 46s 3 meanders + halo strokes + 15 motes The subject
ring 30s 7 concentric ring pairs Foreground drift

Same tile height for all five, different durations — that difference is the parallax. At 11.5s into the loop the layers sit at −92px, −164px, −223px, −300px and −460px respectively.

🔧 How It Works

generate.py
  ├─ meander()    → 3 vertical threads, harmonics of 2πky/H only
  ├─ contours     → 30 polylines, spacing 40px (divides H)
  ├─ dust         → seeded random field, seed = 7
  ├─ constellations → clusters kept 70px off the seam
  └─ rings        → sparse foreground geometry
        ↓
   <defs> holds each layer as a <g id="tile-*">
        ↓
   each layer = <use> + <use transform="translate(0,1200)">
        ↓
   @keyframes scroll { to { transform: translateY(-1200px) } }

A few details worth knowing before editing the generator:

  • Only harmonics of 2πky/H are allowed in anything that crosses the seam. A thread's x(y) is cx + Σ aₖ sin(2πky/H + φₖ) — periodic in H, so position and slope match where the copies meet. Add a term that isn't an integer harmonic and the seam appears as a kink every loop.
  • Contour spacing must divide the tile height. 40px into 1200 is 30 lines; something like 45px leaves a short row at the bottom that reads as a visible band.
  • Per-line variation is driven by phase = 2πy₀/H, so amplitude and wave phase come back to their starting values at the seam. Varying them with raw y₀ instead is the easy way to break an otherwise perfect tile.
  • Constellations stay 70px inside the tile. Their linking lines aren't wrapped, so a cluster straddling the boundary would be sliced in half. Threads and dust don't need the margin — a shape drawn past the edge simply reappears from its second copy.
  • No layer group is clipped. Clipping the tiles would cut the shapes that legitimately overhang the boundary, which is exactly what makes the wrap invisible.
  • Glow is two strokes, not a filter. A wide low-opacity blurred pass under a thin bright pass costs far less than a full-size feGaussianBlur, which is expensive when it covers the whole viewport.

🎛️ Tuning

Everything worth changing lives at the top of generate.py:

Knob Default Effect
random.seed(7) 7 Whole new composition of dust, constellations and rings
W, H 900, 1200 Tile size — H is also the scroll distance
THREADS 3 entries Position, colour, weight, glow and harmonics per thread
LAYERS durations 150 / 84 / 62 / 46 / 30 Parallax spread; larger gaps read as more depth
SPACING 40 Contour density — must divide H
#sky, #bloom, #vig dark blue Background gradients, edited directly in the SVG template

⚙️ Performance

Five composited translateY animations and no per-frame layout, so the whole thing runs on the compositor — effectively free, and it stays at display refresh rate on a phone. The only main-thread cost is the initial parse of ~71 KB of path data.

Note for anyone screenshotting it in CI: headless Chrome does not repaint compositor-driven transforms into --screenshot, so a timed capture looks frozen. Seek the animations first with getAnimations()[0].currentTime = ms and the frame renders correctly.

🛠️ Tech Stack

What How
Artwork Hand-rolled SVG geometry — paths, polylines, circles
Scroll CSS @keyframes on five layer groups
Motes SMIL animateMotion + mpath along the thread paths
Glow Layered strokes plus one small feGaussianBlur
Framing Gradient mask + radial vignette
Generator Python 3, standard library only
Build None

📄 License

MIT © Mark Santos

About

Infinite generative artwork in a single SVG file — five parallax layers scrolling forever, with a mathematically seamless loop and no JavaScript

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages