Every figure, one line of code

Docsv0.1React · JS · Lottie

Install the package, drop a figure in with React or plain JavaScript, and theme it with three CSS properties.

Engine
9 KB
gzipped, no dependencies
Per figure
~2 KB
you only ship what you import
A frame
0.3 ms
drawn live, up to 60 fps
Exports
3
React, plain JS, Lottie

Install

$npm i @piyush_26/mimo

ES modules, no dependencies. The engine is 9 KB gzipped and each figure about 2 KB; a page that imports one figure carries one.

Quick start

tsx
import { Mimo } from '@piyush_26/mimo/react';
import { coffee } from '@piyush_26/mimo/figures';

export default function Contact() {
    return <Mimo figure={coffee} className="w-40" />;
}

The figure renders its still frame on the server, then plays in the browser. It fills its parent's width as a square.

React

<Mimo> renders a <div> and takes any <div> attribute.

PropTypeDefaultWhat it does
figureFigureThe figure to draw, imported from @piyush_26/mimo/figures or your own file.
trigger'hover' | 'loop' | 'once' | 'none'the figure's ownWhen it plays. hover plays while the pointer is over it, loop while on screen, once the first time it scrolls into view.
classNamestringClasses for the wrapper; size it here.

Play on card hover

A hover figure plays while the pointer is over it, or over the nearest ancestor marked data-mimo-hover, and finishes its loop when the pointer leaves, so it always lands on its rest pose. Mark a whole card, and the figure plays wherever the pointer is on it.

tsx
<a href="/contact" data-mimo-hover className="card">
    <Mimo figure={paperPlane} className="w-24" />
    <span>Get in touch</span>
</a>

Plain JavaScript

ts
import { mount } from '@piyush_26/mimo';
import { compass } from '@piyush_26/mimo/figures';

const el = document.getElementById('figure')!;
el.classList.add('mimo');

const player = mount(el, compass, { trigger: 'loop' });
player.destroy();

mount(el, figure, options) returns a player:

MemberWhat it does
play(loop?)Plays once, or loops when loop is true.
stop()Stops and returns to the still frame.
show(frame)Shows one frame of the loop, for scrubbing.
countHow many frames the loop has, at 24 fps.
destroy()Removes the drawing and its listeners.

Still frames

still(figure) returns the rest pose as SVG markup, styles included. Use it for server rendering, emails, or anywhere a figure should not move.

ts
import { still } from '@piyush_26/mimo';
import { summit } from '@piyush_26/mimo/figures';

const svg = still(summit);

Lottie

lottie(figure) returns the figure as Lottie JSON at 12 fps, for native apps, design tools and anything that plays Lottie. It is much heavier than the live engine, so prefer the engine on the web.

The export paints ink #141413, accent #E3DACC and paper #FAF9F5. To theme it with lottie-web's SVG renderer, map those fills to your colours:

css
.player svg [fill='rgb(20,20,19)'] {
    fill: var(--ink);
}
.player svg [fill='rgb(227,218,204)'] {
    fill: var(--accent);
}
.player svg [fill='rgb(250,249,245)'] {
    fill: var(--paper);
}

Theming

Figures read three CSS custom properties, so they follow your theme with no setup.

PropertyDefaultPaints
--mimo-inkcurrentColorevery line
--mimo-accent#e3daccthe accent fills
--mimo-accent-2, --mimo-accent-3--mimo-accentthe extra tints of scattered bits, like confetti
--mimo-paperCanvasthe fills that hide what is behind a part

Set --mimo-paper to the colour of the surface the figure sits on. On a card, that is the card's background.

css
.card {
    background: #f2efe6;
    --mimo-paper: #f2efe6;
}
.dark {
    --mimo-accent: #3d3a34;
}

Performance

Each frame is drawn live, in about 0.3 ms, and redrawn at most 60 times a second. A hover figure only works while it plays; a looping figure stops when it leaves the screen, and browsers pause it in background tabs. Ten figures looping at once cost about 3.6 ms a frame on a laptop.

Loop one figure at a time, a hero for example, and let the rest play on hover.

Accessibility

Each figure is an image named by its own one-line description. When the reader asks for reduced motion, figures show their still frame and never play.

Writing a figure

A figure is a file that ends with figure({ name, means, frames, trigger, draw }), where draw(t) paints the pose at loop progress t. The skill writes these for you; this is the toolkit it draws with.

skills/mimo/kernel.jstext
Space: a 400 × 400 viewBox, x right, y DOWN, origin top left. Keep the
drawing inside 40..360 on both axes: the outer band is breathing room.
Angles are degrees, 0 = pointing right, positive = clockwise on screen.
A shape is an array of [x, y] points; closed shapes carry `.closed = true`.
The kernel's transforms keep that flag; a plain .map() over the points drops it,
so move shapes with move/turn/grow/pose, or rebuild them with poly().

Paint (only inside draw; later calls paint over earlier ones)
  ink(shape, {w, fill})      a hand-inked line along the shape. w scales the line (default 1;
                             0.6 to 1.4 is the useful range). fill: 'accent', 'accent2', 'accent3'
                             or 'paper' fills a
                             closed shape under its line, in one call
  fill(shape, n)             the accent tint, no line: a closed shape. n 2 or 3 picks a second or
                             third tint, only for small scattered bits like confetti (rule 02)
  paper(shape)               the background colour, no line: hides what is behind (a front face)
  solid(shape)               a closed shape filled with ink: dots, pupils, blots
Shapes (all return points; nothing is drawn until you paint them)
  line(points, r)            straight segments through an array of [x, y] points, open; r rounds
                             every joint (default 0). Hand-drawn things have soft corners: use 4 to 12
  poly(points, r)            the same, closed: triangles, flags, polygons
  spline(points, closed)     a smooth curve through the points (Catmull-Rom)
  bez(a, c1, c2, b)          one cubic Bézier, open
  ray(cx, cy, ang, r0, r1)   a straight line pointing out from (cx, cy) at ang, from radius r0 to r1:
                             rays, sparks, spokes
  arc(cx, cy, r, a0, a1)     an arc from angle a0 to a1; a full turn comes back closed
  circle(cx, cy, r)          closed
  ellipse(cx, cy, rx, ry, rot)
  rect(x, y, w, h, r)        a rectangle with corner radius r (cut to half the short side)
  blob(cx, cy, r, {n, wob, seed})   an organic closed shape: n lobes (7), wob 0..0.4 (0.15)
  leaf(x, y, len, ang, wid, curl)   a closed leaf from its base at (x, y), pointing at ang;
                                    curl bends the tip sideways, in viewBox units (+ = clockwise)
Moving shapes
  move(s, dx, dy)            translate
  turn(s, deg, cx, cy)       rotate about (cx, cy)
  grow(s, k, cx, cy)         scale about (cx, cy); k a number or [kx, ky]
  pose(s, {x, y, rot, scale, cx, cy})   all at once: scale and rotate about (cx, cy), then
                             move by (x, y). Missing keys do nothing; (cx, cy) defaults to (200, 200)
  part(s, u0, u1)            the stretch of s between arc-length fractions u0..u1, open:
                             part(s, 0, u) draws a line on; returns [] when u1 <= u0
  at(s, u)                   the point at arc-length fraction u
  dir(s, u)                  the heading in degrees at fraction u
Time (t is the loop's progress, 0 to 1; draw(1) must equal draw(0))
  keys(t, [[t0, v0], [t1, v1, ease?], ...], ease?)
                             the value at t between keyframes. v is a number or an array of
                             numbers (a point, a shape). A stop's third item eases the segment
                             that ends on it; the default is ease.inOut
  span(t, a, b)              0 before a, 1 after b, linear between
  bump(t, a, b)              0 outside a..b, rising to 1 at the middle and back: a there-and-back
  ease.linear .in .out .inOut .back (overshoots by about 10% of the change, then settles:
                             leave room for it in the frame)
  mix(a, b, u), lerp(a, b, u), clamp(v, a, b), rad(deg)
  rand(seed)                 a seeded generator of [0, 1) numbers. Make it INSIDE draw, so every
                             drawing gets the same sequence; Math.random is not allowed

Declaring the figure (the last statement of the file)
  figure({ name, means, frames, trigger, draw })
    name     lowercase, one word or hyphenated
    means    one sentence, at most 140 characters: what the figure shows
    frames   the loop's length, counted at 12 per second: an even number, 24 to 96 (2 to 8 s).
             It plays at the display's refresh rate (exports sample it at 24 fps), so motion
             can be as fast as it needs to be
    trigger  'hover' (plays while the pointer is over it, then finishes its loop), 'loop' (while on screen),
             'once' (the first time it scrolls into view) or 'none'
    draw     (t, k) => void; k is INK