Skip to content
animorio
IT
All components

The player with a wave of lines

Players, Hover, SVGGSAP, CanvasAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

A podcast player in the minimal style: the waveform is a row of thin vertical lines, and the lines already heard take the accent. Passing over with the pointer, the nearby lines grow taller and move apart as under a lens, each on its own spring, while the time of that point, in tabular figures, follows the pointer with the chapter's name. Dragging seeks, play turns into pause and the figures of the time roll one by one.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The player with a wave of lines</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the player with a wave of lines: a podcast player whose waveform is a row of hairlines drawn on a canvas.
         Under the pointer the lines spread and grow as under a lens, each one on its own spring, and the time of the
         point rides above them in tabular figures. No sound: the time simply runs -->
    <section class="player" aria-label="Podcast player" data-player data-autoclic>
      <header class="head">
        <p class="head__show">Slow Signal</p>
        <p class="head__meta">Oct 14 · 48 min</p>
      </header>

      <h2 class="title" data-title>The quiet part of the work</h2>
      <p class="guest">with Tamsin Okoro</p>

      <div class="scope" data-scope>
        <canvas class="scope__lines" aria-hidden="true" data-canvas></canvas>
        <div class="readout" aria-hidden="true" data-readout>
          <span class="readout__time" data-readout-time>0:00</span>
          <span class="readout__chapter" data-readout-chapter></span>
        </div>
        <div class="wave" role="slider" tabindex="0" aria-label="Position in the episode" aria-valuemin="0" data-wave></div>
      </div>

      <div class="controls">
        <button class="play" type="button" aria-label="Play" aria-pressed="false" data-play>
          <svg viewBox="0 0 24 24" aria-hidden="true"><polygon data-a/><polygon data-b/></svg>
        </button>
        <div class="now">
          <p class="now__clock" aria-hidden="true"><span class="digits" data-now>0:00</span><span class="now__of">/</span><span data-total>0:00</span></p>
          <p class="now__chapter" data-chapter></p>
        </div>
        <button class="speed" type="button" aria-label="Playback speed: 1 times" data-speed><span class="speed__in" data-speed-text>1×</span></button>
      </div>
      <p class="sr" aria-live="polite" data-live></p>
    </section>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

39 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The player with a wave of lines” from Animorio and fit it into my site

What it does

On a black background an almost black card, 380 pixels wide, with a thin line round it. At the top Slow Signal and Oct 14 · 48 min in small capitals; below, large, the episode's title, The quiet part of the work, and with Tamsin Okoro. In the middle the waveform: a hundred or so vertical lines one pixel wide, as tall as the voice is loud at that moment (the theme at the start, the sentences, the breaths, a short break at every chapter), with three ticks underneath where the chapters begin. At the bottom the play button, the time 14:05 / 48:30 with the chapter playing, Learning to wait, and the speed 1×. On arrival the words of the title rise and take weight, the lines grow from the left with a small bounce and the orange accent runs up to 14:05, where the listening had stopped. With the pointer over it the nearby lines move apart and grow taller as under a lens, the ones round it close up a little as on the rim of a real lens, and the ones not heard yet light up; each line reaches its place on a spring, one pixel too far and back. Above the lens the time of that point, in tabular figures, follows the pointer on a softer spring, takes weight as the lens opens and narrows when it runs; under it is the chapter's name and a hairline tying it to the line. A click takes the listening there and the accent runs to it; dragging seeks. Play opens into the two bars of pause, the time runs (without sound) and its figures roll one by one; when the chapter changes the name under the time slides away and the new one arrives. The speed goes from 1× to 1.5× and 2×: the figure narrows on the width axis and opens again with the new one.

How it works

Lines from the voice

The episode is a row of 2400 loudness values, written once from a fixed seed: sentences of different lengths with a curve inside, almost silent breaths between them, a slow swell that makes some stretches livelier, the theme at the start and a moment of silence with a short sting at every chapter. The number of lines comes from the width (one every PITCH pixels) and each line mixes the peak and the average of its stretch, with some contrast: at any size the wave stays the same episode.

A lens that does not move the time

Each line has a weight and the strip is shared out in proportion to the weights. The weight is 1, plus a narrow Gaussian round the pointer, minus a wide one that gives back the same area: the lines under the lens take room and the ones in a ring round it give it up, as on the rim of a real lens, while the rest of the strip and its two ends do not move. Each side of the centre pays for its own half, so the line under the pointer is the one that was there before the lens opened and the time read is the right one; near an end, where the ring has few lines to give, that side of the lens grows less, so no weight falls below 0.2 and no line passes another.

A spring for each line

The lens gives each line a place and a height (1 + MAGNIFY times the Gaussian); each line chases them on a damped spring (STIFFNESS, and BOUNCE taking damping away), integrated in steps of 1/120 of a second with the frame's step held to 1/30. The lens's strength comes and goes on a spring with no bounce. The canvas draws the lines in a colour picked from two ramps made beforehand (not heard towards light, heard towards the ink) and a width that grows by half a pixel under the lens; GSAP's ticker comes off when nothing moves.

The time above the lens

The time and the chapter above the strip follow the pointer on a softer spring and stay inside the card; the weight of the type goes from 300 to 530 with the lens's strength and the width (font-stretch, Roboto Flex's wdth axis) drops with the spring's speed, so the figure narrows when it runs. The figures are tabular: the number does not dance while it changes. The width of the writing is measured only when the chapter changes.

Seeking, playing, rolling figures

The time shown is the real time plus a lag that a tween brings to zero in SEEK seconds: a click moves the time at once and the accent runs after it, and dragging (with pointer capture) the time follows the hand while the lag melts away. Play and pause are two polygons whose points slide from the two halves of the triangle to the two bars, with back.out for one pixel too far. The time below is made of little windows, one per figure: only the figure that changes changes, the old one goes up and the new one comes from below. The speed narrows down to 25% of the wdth axis, changes and opens again.

Keys, touch and the preview

The strip is a role slider: arrows of 10 seconds, pages of a minute, Home and End, Space plays and pauses; with the keys the lens rests on the line playing. With a finger the lens opens while the finger touches and stays a moment after, so the time can be read. A click from a script on the card (the library's preview) runs one tour on a GSAP timeline: play, a pointer of its own passing the lens over the wave, pressing and dragging, passing again, leaving, the speed to 1.5×, pause and back to 14:05.

Values to change

NameFileDefaultWhat it changes
LENGTHscript.js'48:30'How long the episode lasts (minutes:seconds, or hours:minutes:seconds).
RESUMEscript.js'14:05'Where the listening had stopped: the player opens there.
CHAPTERSscript.js[['0:00', 'Cold open'], ['6:20', 'Learning to wait'], ['19:45', 'Tools that disappear'], ['34:10', 'The last ten percent']]The chapters: where each one starts and its name. Each has its tick under the wave and a short break in the voice.
SPEEDSscript.js[1, 1.5, 2]The speeds the speed button goes through.
PITCHscript.js3.4The pixels from one line to the next: fewer pixels, more lines.
MAGNIFYscript.js0.85How much taller the lines under the lens get (0 = not at all).
SPREADscript.js1.6How far the lens pushes apart the lines under it (0 = not at all).
REACHscript.js30The radius of the lens, in pixels.
STIFFNESSscript.js320The springs of the lines: higher is quicker.
BOUNCEscript.js0.28How far past their place the lines go before they settle (0 = not at all).
SEEKscript.js0.6The seconds the accent takes to run to a new point.

Try them in the Playground

When to use it

For a podcast's site, an episode's page, an audio interview, the recording of a talk, an audiobook or an app's voice note: a sober player whose wave reads like an index and where the right point is found at a glance.

On phones

The card is at most 380 pixels wide (at 390 it is 358 wide and the lines are counted again from the width). A finger touching the strip opens the lens and drags it without scrolling the page (touch-action none only on the strip); lifting it the lens stays 0.9 seconds, so the time can be read. The play button is 54 pixels.

With reduced motion

The lines do not move or grow: the lens only lights them, with a fade. The time above the strip fades in and sits under the pointer with no spring; the accent jumps straight to the new point, the figures and the chapter change without rolling, the play icon changes at once and the card fades in, already at 14:05.

Pitfalls

  • With the strip shared out in proportion to the weights, the lens pulled the whole wave: every far line closed up, the lens opened a few pixels beside the pointer and the time under the finger changed by two minutes while it opened. Now the extra weight under the lens is given up by a ring round it, and each side pays for itself: the rest of the strip stays still and the line under the pointer does not change.
  • Near the ends the ring has few lines to take room from and the weights went below zero (lines passing each other): that side of the lens grows less, just enough for every weight to stay at least 0.2.
  • With the two figures rolling on two different curves (one slow to start, the other quick) for a moment both showed in the little window: now they go up and down with the same start.
  • The line playing was white but, in a quiet spot, one pixel tall and impossible to find: it is always at least 70% as tall as a loud line.
  • Fontsource's index.css has only the wght axis: for the width you need Roboto Flex's wdth.css, which brings wght and wdth together.

Libraries

GSAP 3.15.0, Roboto Flex (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP 28 KB.

Testing

Tested on 16 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components