Guide

Pluton 2D is a library for technical drawing in SVG. It gives you clean geometry, dimensions, and stable rendering without fighting the DOM.

Install and setup

npm install pluton-2d

Import the stylesheet first. It provides default styling for the grid, axes, and dimensions. Then create a scene by passing an SVG element and your parameters. The SVG needs either CSS width/height or a viewBox attribute so Pluton knows the canvas size:

import "pluton-2d/style.css";
import { Pluton2D } from "pluton-2d";

const svg = document.querySelector("svg")!;
const scene = new Pluton2D(svg, {
  params: { width: 200, height: 100 },
});

scene.enablePan(true);
scene.enableZoom(true);

const geom = scene.geometry.group();

scene.draw((p) => {
  const path = geom.path();
  path
    .moveToAbs(-p.width / 2, -p.height / 2)
    .lineTo(p.width, 0)
    .lineTo(0, p.height)
    .lineTo(-p.width, 0)
    .close();
});

You’ll want to create groups once, outside your draw callback. The reason is simple: groups persist across frames, while builders get recycled. Create the group early, then request builders inside your draw function each time it runs. The engine only writes changed attributes to the DOM, so you don’t pay for work that didn’t happen.

Reactive params

The params object becomes reactive through a Proxy. When you mutate properties, the scene redraws automatically. Redraws are throttled at 60 FPS, so you can update parameters as often as you like without worrying about performance:

scene.params.width = 300; // triggers redraw

Object.assign(scene.params, { width: 250, height: 150 }); // also works

// Don't reassign the whole object. The Proxy is on the original
// scene.params = { ... };  ← won't trigger redraws

Parameters need to be a flat object. Nested objects will throw an error at construction time. The reason is simple: the reactivity system only tracks mutations at the top level. If you need to organize complex state, keep it outside scene.params and sync the flat values inside your draw callback.

When you’re done, call scene.dispose() to clean up listeners and DOM.

Coordinate system

Pluton uses a center-origin, Y-up coordinate system. This is the same convention you’d use in mathematics or engineering drawings, not typical screen coordinates where Y goes down. Here’s what that looks like:

  • Origin: center of viewport
  • Positive X → right
  • Positive Y → up
  • lineTo(10, 20) moves right 10, up 20

Under the hood, the rendering layer flips Y-coordinates so your math-style coordinates map correctly to SVG. Arc directions are adjusted automatically. When you specify clockwise = true, it actually renders clockwise on screen.

Static vs dynamic

When you have geometry that doesn’t change frame-to-frame (background grids, reference shapes, fixed annotations), mark the group as static. After the first commit, the engine skips all DOM writes for that group. This improves performance when you mix static and dynamic content, since the engine only updates what actually changed:

const bg = scene.geometry.group();
bg.setDrawUsage("static"); // commits once

const fg = scene.geometry.group(); // dynamic by default

scene.draw((p) => {
  // Static. Still call path() every frame, engine skips DOM write
  const bgPath = bg.path();
  bgPath.moveToAbs(-100, -100).lineTo(200, 0).lineTo(0, 200).close();

  // Dynamic. DOM updates every frame
  const fgPath = fg.path();
  fgPath.moveToAbs(0, 0).lineTo(p.width, 0).lineTo(0, p.height).close();
});

After the first commit, static groups skip DOM writes. Calling clear() or setDrawUsage("dynamic") resets the flag if you need to change the geometry later.

Use static for background grids, fixed annotations, or reference shapes. Use dynamic for anything that responds to params or user input.

Styling

All styling uses CSS custom properties on .pluton-root. This means you can override the defaults per-instance without touching JavaScript. Here are the available variables and their defaults:

.pluton-root {
  --pluton-grid-minor-stroke: rgba(0, 0, 0, 0.025);
  --pluton-grid-major-stroke: rgba(0, 0, 0, 0.12);
  --pluton-grid-stroke-width: 0.5;

  --pluton-axis-color: rgba(0, 0, 0, 0.2);
  --pluton-axis-stroke-width: 1;
  --pluton-axis-dash: 5 5;

  --pluton-geometry-stroke: rgba(0, 0, 0, 0.7);
  --pluton-geometry-stroke-width: 1;

  --pluton-hatch-color: rgba(0, 39, 50, 0.14);

  --pluton-dim-color: black;
  --pluton-dim-stroke-width: 1;
  --pluton-dim-text-color: rgba(0, 0, 0, 0.6);
  --pluton-dim-font-size: 12px;
  --pluton-dim-font-family: system-ui, sans-serif;
}

Custom classes

When you need more specific control than the CSS variables provide, you can pass className to path() or dimension():

const path = geom.path({ className: "my-path" });

// In CSS:
// .pluton-root .pluton-geometry path.my-path { stroke: #e11d48; }

path() also accepts fill, stroke, and fillRule when you need hatch fills, custom colors, or cutouts. Geometry groups support scale(x, y) and translate(x, y) for positioning.

Hatch fill

Hatching is a traditional drafting technique where parallel lines indicate filled areas. It’s been used in technical drawings for decades to show different materials or filled regions. You can create hatch patterns with addHatchFill(color, opacity?), which returns a fill ID. Pass that ID to path({ fill }) to apply the pattern:

const blueFill = scene.addHatchFill("#2563eb");
const orangeFill = scene.addHatchFill("#ea580c", 0.45);

geom.path({ fill: blueFill }).moveToAbs(...).lineTo(...).close();
geom.path({ fill: orangeFill, fillRule: "evenodd" }).moveToAbs(...).close();

With fills enabled (default), each path uses whatever you specify in path({ fill }), or falls back to the built-in default hatch. You can toggle all fills with scene.enableFill(false) to hide them, or scene.enableFill(true) to show them. For stroke-only geometry, set fill: "none" on the path or in CSS.

Pencil filter

The pencil filter gives geometry a hand-drawn look. Useful for sketches or informal diagrams. You can adjust the intensity at runtime. The filter can be expensive on Safari during zoom, especially with many paths:

scene.enableFilter(true);
scene.setFilterIntensity(1.25); // default

// Can also call inside draw callbacks
scene.draw(() => {
  scene.setFilterIntensity(1.25);
});

Camera controls

Pan and zoom are opt-in. Enable them when you need interactive navigation:

scene.enablePan(true); // middle-mouse drag, or shift + left-click
scene.enableZoom(true); // mouse wheel, 1×-20× range

scene.resetCamera(); // smooth animation back to origin

Camera movement is smoothed with exponential easing, which makes navigation feel natural and responsive. resetCamera() animates instead of jumping, so the transition feels smooth.

Responsive scaling

For mobile and tablet devices, you can scale the entire view down to give geometry more breathing room. This is different from camera zoom. It scales the output without changing your coordinate system or affecting pan/zoom behavior:

// Using matchMedia for clean breakpoint handling
const mobileQuery = window.matchMedia("(max-width: 640px)");
const tabletQuery = window.matchMedia(
  "(min-width: 641px) and (max-width: 1024px)",
);

const updateScale = () => {
  if (mobileQuery.matches) {
    scene.setViewScale(0.7); // mobile: 70% scale
  } else if (tabletQuery.matches) {
    scene.setViewScale(0.85); // tablet: 85% scale
  } else {
    scene.setViewScale(1.0); // desktop: full scale
  }
};

updateScale();
mobileQuery.addEventListener("change", updateScale);
tabletQuery.addEventListener("change", updateScale);

The scale interpolates smoothly like pan and zoom. Camera state is preserved. If the user zoomed in 3×, they’ll still be at 3× zoom after the view scale changes. The coordinate system stays the same, so moveTo(100, 100) still means the same world position regardless of view scale.

When to use Pluton 2D

Pluton was built for technical drawing: crisp SVG, hatching, dimensions, and low DOM churn. You can use it for other SVG work, but it’s not trying to replace everything. Here’s how to think about when it’s a good fit.

Good fit

  • Technical drawings, blueprints, engineering diagrams
  • Annotation-heavy scenes (dimensions, ticks, callouts)
  • Inspectable or exportable SVG workflows
  • Interactive scenes with moderate redraw frequency

Charting libraries might be a better fit when

  • Your goal is data visualization with standard chart types
  • You need chart primitives and conventions out of the box (scales, legends, tooltips)
  • You’re working with time-series data, bar charts, or scatter plots

Canvas might be a better fit when

  • You’re working with high-frequency animation (think 60+ shapes moving per frame)
  • You have large numbers of moving primitives and hit SVG/DOM performance limits
  • You don’t need inspectable, exportable SVG output

WebGL/WebGPU might be a better fit when

  • You need GPU-driven rendering or advanced visual effects
  • You’re building shader-driven pipelines or post-processing chains
  • You’re working with 3D or large-scale real-time rendering