Skip to content

SVG · 2D and 3D · themeable

cseen

Themeable 2D and 3D SVG illustrations and animations for the web.

Describe a scene in a few lines of TypeScript, animate it on a timeline, and put it on a page. It draws real SVG, so it follows your CSS, renders on the server, and plays live in the browser.

npm install cseen
Six squares are drawn on the ground, each gets its letter, then the net folds shut into a cube that turns on its base. The home page uses it.

See it move

  • LaptopScreen, keyboard and trackpad. lean tilts the screen back.

    Devices

    Monitors, laptops, routers, a keyboard.

  • Typing code on a screenScreens have code(), and typeOn types it letter by letter.1const s = new Scene();2s.add(ground(),laptop());34// animate it5tl.play(fadeIn(laptop));

    Animation

    Scrub, pause, render one frame.

  • InterferenceTwo sources, with the lines where the waves cancel.

    Waves and optics

    Sonar, stereo rigs, lenses.

  • Progress and loadingBars, rings and segments with an animatable value. Done 100%100%

    Interfaces

    Progress, chat and dialogs on screens.

  • A flowchartStart, a decision and two ways to the end. Edges trace on one after another.startsigned in?sign inhomeyesno

    Graphs and flow

    Flowcharts, trees and networks that place themselves.

A scene in a few lines

Letters that trade placescore
scene.ts
import { Construct, Scene, Timeline, ground, group, par, tween } from 'cseen';

// The letters of the logo, c s e e n, stand on five invisible squares. They trade places to spell s c e n e, then trade back, again and again.
const s = new Scene({ view: { yaw: -16, pitch: 40 }, size: [640, 250], padding: 16 });
s.add(ground());
const stage = s.add(group());
const flat = { u: [1, 0, 0] as V3, v: [0, 1, 0] as V3 };
// five squares of 120 with 30 between, 100 to the world unit; the letters move over them (the squares themselves are not drawn)
const row = stage.add(new Construct({ ...flat, box: [0, 0, 720, 120], size: [7.2, 1.2] }));
const [rx, ry] = row.at(360, 60);
stage.props.pos = [-rx, -ry, 0];

// each letter is outlines on a square of 100 (y runs down), with the strokes 18 thick, like the mark of the logo; the e has a hole
const glyphs: Record<string, string[]> = {
  c: ['0 0 100 0 100 18 18 18 18 82 100 82 100 100 0 100'],
  s: ['0 0 100 0 100 18 18 18 18 41 100 41 100 100 0 100 0 82 82 82 82 59 0 59'],
  e: ['0 0 100 0 100 59 18 59 18 82 100 82 100 100 0 100', '18 18 18 41 82 41 82 18'],
  n: ['0 0 100 0 100 100 82 100 82 18 18 18 18 100 0 100'],
};
const points = (l: string) => { const n = l.split(' ').map(Number); return n.filter((_, i) => i % 2 === 0).map((x, i): Pt => [x, n[2 * i + 1]]); };
// two colours, as in the logo: c and n in the accent, s and both e in ink
const INK = 'ink', ACCENT = 'accent';
const word = 'cseen', colours = [ACCENT, INK, INK, INK, ACCENT], after = [1, 0, 2, 4, 3];   // after[i] is the square that letter i swaps to
const PITCH = 1.5, SIDE = 1.2, K = SIDE / 100, THICK = 0.3;
const sample = new Construct({ ...flat, box: [0, 0, 100, 100], size: [SIDE, SIDE] });
const [cx, cy] = sample.at(50, 50);
const letters = [...word].map((name, i) => {
  const [wx, wy] = row.at(60 + 150 * i, 60);
  const lift = stage.add(group());                                 // lifts the letter over the others
  const home: V3 = [wx - cx, wy - cy, 0], slide = lift.add(group({ at: home }));    // carries it to its other square
  const goal: V3 = [home[0] + (after[i] - i) * PITCH, home[1], 0];
  const shade = `color-mix(in oklab, var(--c-${colours[i]}) 70%, var(--c-bg))`;
  // the top of the letter, raised by its thickness, and a wall standing on each edge of its outline
  const top = slide.add(new Construct({ ...flat, box: [0, 0, 100, 100], size: [SIDE, SIDE], at: [0, 0, THICK] }));
  top.add({ d: glyphs[name].map((l) => 'M' + points(l).map((q) => q.join(' ')).join('L') + 'Z').join(''), paint: colours[i], stroke: 'none', props: { fill: 1 } });
  for (const l of glyphs[name]) {
    const ring = points(l);
    ring.forEach(([x, y], k) => {
      const [x2, y2] = ring[(k + 1) % ring.length], dx = (x2 - x) * K, dy = -(y2 - y) * K, len = Math.hypot(dx, dy);
      const wall = slide.add(new Construct({ box: [0, 0, len * 100, THICK * 100], size: [len, THICK], at: [x * K, (100 - y) * K, 0], u: [dx / len, dy / len, 0], v: [0, 0, 1] }));
      wall.add({ d: `M0 0H${len * 100}V${THICK * 100}H0Z`, paint: shade, stroke: shade, width: 1, props: { fill: 1 } });
    });
  }
  return { lift, slide, home, goal };
});

const soft = (t: number) => (1 - Math.cos(Math.PI * t)) / 2;
const t = (p: Animatable, to: Record<string, number | number[]>, from: Record<string, number | number[]>, at: number, len: number) =>
  tween(p, to, { from, delay: at, duration: len, ease: soft });
// all the letters swap in pairs: the c steps over the s, and the n over the second e; `flip` is which way
const swap = (flip: boolean): Anim => par(...letters.flatMap((l, i) => {
  if (l.goal[0] === l.home[0]) return [];
  const [a, b] = flip ? [l.home, l.goal] : [l.goal, l.home], at = i > 2 ? 0.5 : 0, over = l.goal[0] > l.home[0];
  return [t(l.slide, { pos: b }, { pos: a }, at, 1.6), ...(over ? [t(l.lift, { pos: [0, 0, 1.1] }, { pos: [0, 0, 0] }, at, 0.8), t(l.lift, { pos: [0, 0, 0] }, { pos: [0, 0, 1.1] }, at + 0.8, 0.8)] : [])];
}));

const tl = new Timeline();
// c s e e n  ->  s c e n e  ->  c s e e n, and round again
tl.wait(1.6);
tl.play(swap(true));
tl.wait(1.6);
tl.play(swap(false));
tl.wait(0.4);
Letters that trade placesThe letters c s e e n stand on five squares, c and n in the accent colour. The c steps over the s and the n over an e, so the word reads s c e n e, then they swap back, in a loop.

Try it in any theme. Three CSS variables and everything else, the figure included, is derived from them.

What it does

  • 01

    One space for 2D and 3D

    x to the right, y into the scene, z up. A flat diagram is a scene seen from the front.

  • 02

    Real SVG

    It follows your CSS, renders on the server, and plays live in the browser. Nothing to rasterise.

  • 03

    A scene is a function of time

    Playing, pausing, stepping and rendering a single frame all work the same way.

  • 04

    Pick what you use

    The core is small. Every pack has its own import, so a page that needs a monitor does not load the charts.

  • 05

    Ready-made parts

    Monitors, laptops, routers, charts, waves, lenses, graphs and flowcharts are built in. Place them, do not draw them.

  • 06

    Camera and orbit

    Parallel or perspective, and the camera animates like anything else. Let readers drag to turn, zoom and reset.

  • 07

    Follows your theme

    Set up to three CSS variables on a figure or any parent, and every colour is derived from them. Light and dark come with it.

  • 08

    Show how it is made

    Build a drawing from guides, circles and outlines that trace on, fill and turn, then take it apart again. The cube above is built this way.

Pick what you use

Learn it in four steps

  1. 1InstallOne package, one stylesheet.
  2. 2Your first sceneDraw, animate and embed in ten minutes.
  3. 3Theme itThe three-variable contract behind every colour.
  4. 4Look things upEvery export, generated from the code.