npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

twistysvg

v0.1.0

Published

Render, animate, and interact with twisty puzzles as SVG — React components for NxN cubes, pyraminx, skewb, megaminx, square-1, clock, and FTO with alg playback, masks, arrows, PNG/GIF export

Readme

twistysvg

Render, animate, and interact with twisty puzzles as SVG — in React or anywhere. NxN cubes, Pyraminx, Skewb, Megaminx, Square-1, Clock, and FTO (face-turning octahedron).

A modern successor to puzzle-gen / visualcube: the same case-diagram vocabulary (masks, sticker colors, arrows, plan view), plus things they don't do — smooth turn animation with play/pause/step/seek, drag-to-turn interaction, SSR-safe React components, and PNG/GIF export.

  • Zero-dependency core (twistysvg): cube state, move notation, geometry, scene computation, playback engine. Runs in Node and the browser.
  • React bindings (twistysvg/react): <Cube>, <CubePlayer>, <InteractiveCube>, useAlgPlayer, useCubeState. React is an optional peer dependency.
  • Export (twistysvg/export): SVG / PNG / animated GIF.

All WCA puzzles are supported with full parity: static images, scramble/alg application in each puzzle's official notation, smooth turn animation with play/pause/step/seek, drag interaction, and PNG/GIF export.

| puzzle | notation | extras | | --- | --- | --- | | NxN cubes ('cube', size) | R U' Rw 3Rw M x … | plan view, mask presets, hint facelets, transparent | | Pyraminx | U L R B + tips u l r b | | | Skewb | WCA fixed-corner U L R B | | | Megaminx | 12 face names + R++ D-- U' (verified against TNoodle) | LL plan view | | Square-1 | (3,-2) / with slice-legality checking | shape-shifting render, layer-drag + tap-to-slice | | Clock | UR3+ DL2- y2 + trailing pins | front+back 2D view | | FTO | U F R L D B BR BL | face-turning octahedron with corner-tip layers |

Install

npm install twistysvg

Static images

import { Cube } from 'twistysvg/react';

// a scrambled cube
<Cube setup="R U R' U' F2 D L B'" />

// an OLL case diagram: `case` shows the state the alg solves
<Cube view="plan" mask="oll" case="r U R' U' r' F R F'" />

// PLL diagram with arrows
<Cube
  view="plan"
  mask="pll"
  case="R U R' U' R' F R2 U' R' U' R U R' F'"
  arrows={[{ from: 'U2', to: 'U8', headStart: true }]}
/>

Components are SSR-safe: the output is plain <svg> with no ids, no defs, and no effects required, so it renders identically on the server.

Props (shared visual vocabulary)

| prop | meaning | | --- | --- | | size | cube size N (default 3) | | setup | alg applied to the solved cube | | case | inverse-of-alg applied (visualcube semantics for case diagrams) | | view | '3d' (default) or 'plan' (top-down case diagram) | | camera | { yaw, pitch, projection, distance } | | scheme | color scheme overrides (U R F D L B, body, dim, arrow, background) | | mask | preset name ('oll', 'pll', 'f2l', 'cll', 'ell', 'cross', …) or per-sticker { U4: 'hidden' } | | stickerColors | raw color per sticker, e.g. { U0: '#e91e63' } | | arrows | { from, to, via?, color?, width?, headStart?, headEnd? }[] | | hintFacelets | show hidden faces floating behind the cube (true or a gap distance); they animate with turns and are draggable in <InteractiveCube> | | transparent | see-through cube: no plastic, translucent stickers, back faces visible |

Stickers are addressed as U0U8, F4, etc. — row-major per face in the standard Kociemba/visualcube reading order, so existing visualcube configurations translate directly.

Hidden faces

// twizzle-style hint facelets: hidden faces float behind the cube,
// animate with turns, accept arrows, and stay draggable
<Cube setup="R U R' U'" hintFacelets />
<Cube setup="R U R' U'" hintFacelets={4} /> // wider gap

// see-through cube: no plastic, translucent stickers
<Cube setup="R U R' U'" transparent />

Any WCA puzzle

import { TwistyPuzzle, TwistyPlayer, usePuzzlePlayer, InteractivePuzzle, InteractiveSquare1 } from 'twistysvg/react';

// static images in each puzzle's notation
<TwistyPuzzle puzzle="pyraminx" setup="U L' R B u'" />
<TwistyPuzzle puzzle="megaminx" view="plan" case="R U R' U R U2' R'" />
<TwistyPuzzle puzzle="clock" setup="UR3+ DL2- ALL4+ y2 UR" />

// animated player for any puzzle
const handle = usePuzzlePlayer({ puzzle: 'square1', alg: '(1,0) / (3,-3) /' });
<TwistyPlayer player={handle} controls />

// drag-to-turn for the rotation puzzles; square-1 gets layer dragging
// with 30° snapping and tap-to-slice (with legality checking)
<InteractivePuzzle puzzle="skewb" onMove={(turn, notation) => console.log(notation)} />
<InteractiveSquare1 onIllegalSlice={() => flash()} />

Animated player

import { CubePlayer, useAlgPlayer } from 'twistysvg/react';

function TPerm() {
  const handle = useAlgPlayer({
    alg: "R U R' U' R' F R2 U' R' U' R U R' F'",
    tps: 2.5, // quarter turns per second
    loop: true,
  });
  return <CubePlayer player={handle} controls />;
}

handle.player exposes the full imperative API: play() pause() stop() seekTo(ms) seekToMove(i) stepForward() stepBackward() setSpeed(tps) setAlg(alg). The handle's status, currentMoveIndex, moves, and totalMs are reactive.

Animation never re-renders your React tree: per-frame updates patch SVG attributes directly through a keyed differ, while React only re-renders on status/move transitions.

Interactive cube

import { InteractiveCube } from 'twistysvg/react';

<InteractiveCube
  defaultState="R U R' U' F2 D"
  onMove={(move, notation, next) => console.log(notation)}
/>

Drag a sticker to turn its layer (the drag direction picks the move); drag the background to orbit. Pointer Events only — works with touch.

Headless core

import { applyAlg, createSolved, computeScene, resolveSceneInput, sceneToSvgString } from 'twistysvg';

const state = applyAlg(createSolved(3), "R U R' U'");
const svg = sceneToSvgString(computeScene(resolveSceneInput({ state, mask: 'll' })));

computeScene is a pure function from state + camera (+ partial turn) to ordered, culled 2D polygons; the SVG string renderer, the imperative SVG DOM patcher, and the canvas rasterizer all consume the same scenes. Mid-turn rendering splits the cube into convex slabs ordered by the cut planes, so there is no depth sorting to glitch.

Export

import { exportSvg, exportPng, exportGif, downloadBlob } from 'twistysvg/export';

const png = await exportPng({ setup: 'R U F' }, { width: 512 });

const gif = await exportGif(
  { alg: "R U R' U'", setup: '' },
  { tps: 2.5, fps: 25, width: 320, loop: true, onProgress: (done, total) => {} },
);
downloadBlob(gif, 'sexy-move.gif');

GIF encoding (gifenc) runs in the browser, renders frames straight to canvas (no SVG rasterization round trip), supports AbortSignal, and yields to the event loop while encoding.

Notation

U D L R F B with ', 2, 2'; wide moves Rw / r / 3Rw; slices M E S; rotations x y z; parentheses for grouping. Parse errors carry the offending token's offset.

Known limitations

  • Square-1 slice strobing: the 180° slice flip can shimmer — seam lines on the flipping half move many times their width per frame (temporal aliasing inherent to fast flat-shaded flips; measured against a 3x3 cube baseline with the Playwright harness in scripts/flicker-hunt.mjs). Mitigations shipped: exact painter ordering, solid seam geometry, a mid-flip deceleration warp, and slower slice timing. Slowing slices further reduces it; a shaded (WebGL) renderer would eliminate it.
  • Mask presets are cube-only; plan view is cube + megaminx; clock is render-only (no drag interaction).

Development

npm install
npm run dev        # Vite playground
npm test           # Vitest
npm run build      # tsup → dist (ESM + CJS + d.ts)

License

MIT