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

lego-image

v2.0.2

Published

Canvas image cropper for JavaScript and React — pan, zoom, rotate, frame resize, export, and persist transform data.

Readme

lego-image

Canvas-based image cropper for JavaScript and React. Pan, zoom, rotate, resize the frame, export canvas output, and persist transform data for later editing.

Install

npm install lego-image

Contents

Quick start

import initLego from "lego-image";

const canvas = document.querySelector("#canvas");

const cropper = initLego({
  canvas,
  onChange: (values) => {
    console.log(values);
  },
});

// First load — cover fit at default frame size
await cropper.setLego({
  imageUrl: "https://example.com/photo.png",
  imageType: "image/png",
  frameSize: { width: 600, height: 600 },
});

Data format (canonical)

Use the same object shape for first load, restore, and database storage.

setLego config

{
  imageUrl: string;              // required — URL, blob URL, or data URL
  imageType?: string;            // optional — "image/png" | "image/jpeg" | "image/webp"
  frameSize?: {                  // optional — canvas frame size in px
    width: number;
    height: number;
  };
  transform?: {                  // optional — omit on first load; pass on restore
    x: number;                   // visual top-left X as fraction of frame width (CSS translate)
    y: number;                   // visual top-left Y as fraction of frame height (CSS translate)
    width: number;               // image width as fraction of frame width
    height: number;              // image height as fraction of frame height
    rotate: number;              // degrees
    scale?: number;              // extra zoom above cover fit; 0 = minimum (cover)
  };
}

First load (no saved edit yet)

await cropper.setLego({
  imageUrl: "https://example.com/photo.png",
  imageType: "image/png",
  frameSize: { width: 600, height: 600 },
});

Omit transform — the cropper applies default cover fit.

Restore saved edit (user returns)

const saved = await fetchFromDatabase(userId);

await cropper.setLego({
  imageUrl: saved.imageUrl,
  imageType: saved.imageType,
  frameSize: saved.frameSize,
  transform: saved.transform,
});

Example saved record (database)

{
  "imageUrl": "https://cdn.example.com/uploads/user-123.png",
  "imageType": "image/png",
  "frameSize": { "width": 600, "height": 600 },
  "transform": {
    "x": 0.32,
    "y": -0.15,
    "width": 1.6,
    "height": 1.6,
    "rotate": 45,
    "scale": 0
  }
}

Important: Save transform from onChange. Do not use deprecated pos, scale, or rotate.


Initialization

initLego({ canvas, onChange })

| Option | Type | Required | Description | |--------|------|----------|-------------| | canvas | HTMLCanvasElement | Yes | Canvas element used for rendering | | onChange | function | Yes | Called whenever transform changes |

Returns a cropper API object.

Recommended canvas setup

<div id="frame" style="width: 600px; height: 600px; overflow: hidden;">
  <canvas id="canvas"></canvas>
</div>

Pass frameSize in setLego or call setFrameSize separately. Do not bind React width/height props on <canvas> — let the cropper control dimensions to avoid clearing the bitmap on re-render.


API reference

cropper.setLego(options)Promise

Loads an image and initializes or restores the cropper.

await cropper.setLego({
  imageUrl: "https://cdn.example.com/user.png",
  imageType: "image/png",
  frameSize: { width: 600, height: 600 },
  transform: {
    x: 0.32,
    y: -0.15,
    width: 1.6,
    height: 1.6,
    rotate: 45,
    scale: 0,
  },
});

| Option | Type | Description | |--------|------|-------------| | imageUrl | string | Image URL, blob URL, or data URL | | imageType / mimeType | string | MIME type for blob/data URLs and export | | frameSize | { width, height } | Canvas frame size in pixels | | transform | object | Saved transform from onChange — restores edit state |

Deprecated options (backward compatible — will be removed in a future version)

| Option | Type | Replacement | |--------|------|-------------| | pos | { x, y } | transform.x, transform.y | | scale | number | transform.scale (or transform.width / transform.height) | | rotate | number | transform.rotate |

Using these fields logs a console warning:

[lego-image] Deprecated: `pos`, `scale`, and `rotate` in setLego() are deprecated.
Pass `transform` from onChange instead.

cropper.resetImage()

Resets pan, zoom, and rotation to default cover fit for the current frame.


Zoom

| Method | Description | |--------|-------------| | cropper.zoomIn() | Zoom in by 1 step (0.01 scale) | | cropper.zoomOut() | Zoom out by 1 step | | cropper.zoomToScale(scale) | Set zoom level; 0 = cover fit minimum, higher = more zoom | | cropper.getZoomLimits() | Returns { min, max, scale, minLevel, maxLevel, level } — all relative, min is 0 |

Mouse wheel zoom: hold Ctrl (or Cmd) and scroll on the canvas.


Rotation

| Method | Description | |--------|-------------| | cropper.rotateTo(degrees) | Set absolute rotation | | cropper.rotateBy(delta) | Rotate by delta degrees | | cropper.rotateRight() | Rotate +90° | | cropper.rotateLeft() | Rotate -90° |


Frame size

| Method | Description | |--------|-------------| | cropper.setFrameSize(width, height) | Resize canvas frame (async, remounts image) | | cropper.getFrameSize() | Returns { width, height } |

When frame size changes, rotate and scale are preserved; width, height, x, and y are recalculated from the same anchor edge/center.

await cropper.setFrameSize(800, 400);

Or pass frameSize inside setLego.


Export canvas as data URL

| Method | Description | |--------|-------------| | cropper.getCanvasDataUrl(type?, quality?) | Returns base64 data URL or null | | cropper.getSourceMimeType() | MIME type of loaded source image | | cropper.getExportMimeType() | MIME type used for auto export |

Auto format (no arguments):

| Source | Export | |--------|--------| | PNG | image/png (transparency preserved) | | WebP | image/webp (falls back to PNG) | | JPEG / other | image/jpeg |

const dataUrl = cropper.getCanvasDataUrl();
const jpeg = cropper.getCanvasDataUrl("image/jpeg", 0.92);

Transform coordinates

The cropper renders on canvas with rotation around the frame center. Internally it tracks a host position for pan/zoom math; onChange converts that to visual top-left coordinates so saved values match what you see on screen and in CSS.

| Value | Meaning | |-------|---------| | x, y | Visual top-left position as fraction of frame — use directly in CSS translate(x * frameW, y * frameH) | | width, height | Drawn image size as fraction of frame | | rotate | Rotation in degrees around the frame center | | scale | Extra zoom above cover fit; 0 = minimum (image covers frame) |

Canvas render model

translate(imageCenter offset from frame center, rotated)
rotate(angle)
drawImage centered on that point

Restore

Pass the saved transform back to setLego as-is. The library converts visual x / y back to the internal position automatically.


Transform output (onChange)

Called on every pan, zoom, rotate, or frame change. Save this object (or a subset) to your database.

onChange: (values) => {
  // {
  //   x: 0.32,           // visual top-left X (fraction of frame width)
  //   y: -0.15,          // visual top-left Y (fraction of frame height)
  //   width: 1.6,
  //   height: 1.6,
  //   rotate: 45,
  //   scale: 0              // 0 = cover fit minimum; increases when zoomed in
  // }
}

| Field | Type | Store? | Description | |-------|------|--------|-------------| | x | number | Yes | Visual top-left X as fraction of frame width | | y | number | Yes | Visual top-left Y as fraction of frame height | | width | number | Yes | Image width as fraction of frame width | | height | number | Yes | Image height as fraction of frame height | | rotate | number | Yes | Rotation in degrees (pivot: frame center) | | scale | number | Optional | Extra zoom above cover fit; minimum 0 (width / height define actual size) |


Converting transform values to useful values

onChange returns fractions (relative to frame size). Your app usually needs pixels for CSS/DOM, or the same fractions for save/restore.

Inputs you always need

const frameW = frameSize.width;   // e.g. 600
const frameH = frameSize.height;  // e.g. 600

// from onChange:
const { x, y, width, height, rotate, scale } = values;

Fraction → pixel (most common)

| Useful value | Formula | Used for | |--------------|---------|----------| | left | x * frameW | CSS translateX | | top | y * frameH | CSS translateY | | imageWidth | width * frameW | CSS width | | imageHeight | height * frameH | CSS height | | rotateDeg | rotate | CSS rotate() — already in degrees | | zoomScale | scale | Extra zoom above cover; 0 = minimum |

function transformToPixels(values, frameW, frameH) {
  return {
    left: values.x * frameW,
    top: values.y * frameH,
    width: values.width * frameW,
    height: values.height * frameH,
    rotate: values.rotate ?? 0,
    scale: Math.max(0, values.scale ?? 0),
  };
}

// Example onChange output:
// { x: 1.51, y: -0.89, width: 2.66, height: 1.28, rotate: 45, scale: 1.06 }
// frameSize: { width: 600, height: 600 }

const px = transformToPixels(values, 600, 600);
// {
//   left: 906,
//   top: -534,
//   width: 1596,
//   height: 768,
//   rotate: 45,
//   scale: 1.06          // 0 at cover fit; ~1.06 = zoomed in above minimum
// }

Apply to CSS (DOM preview)

const px = transformToPixels(values, frameW, frameH);

element.style.width = `${px.width}px`;
element.style.height = `${px.height}px`;
element.style.transform =
  `translate(${px.left}px, ${px.top}px) rotate(${px.rotate}deg)`;

Fixed CSS on the element:

position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;

Pixel → fraction (build transform for setLego)

Only needed if you store pixels instead of fractions. Normally save fractions from onChange directly.

function pixelsToTransform(px, frameW, frameH) {
  return {
    x: px.left / frameW,
    y: px.top / frameH,
    width: px.width / frameW,
    height: px.height / frameH,
    rotate: px.rotate ?? 0,
    scale: px.scale ?? 0,
  };
}

What to save vs what to compute

| Save to database | Compute in UI when needed | |------------------|---------------------------| | x, y, width, height, rotate | left, top, imageWidth, imageHeight (pixels) | | scale (optional) | CSS transform string | | frameSize.width, frameSize.height | — |

Rule: store fractions from onChange. Convert to pixels only when rendering HTML/CSS.

Quick reference

onChange (fractions)          Your UI (pixels)
─────────────────────         ──────────────────
x              × frameW   →   left
y              × frameH   →   top
width          × frameW   →   imageWidth
height         × frameH   →   imageHeight
rotate         (as-is)    →   rotateDeg
scale          (as-is)    →   zoomScale (0 = cover minimum)

Save to database

let savedRecord = {
  imageUrl: "",
  imageType: "",
  frameSize: { width: 600, height: 600 },
  transform: null,
};

const cropper = initLego({
  canvas,
  onChange: (values) => {
    savedRecord.transform = {
      x: values.x,
      y: values.y,
      width: values.width,
      height: values.height,
      rotate: values.rotate,
      scale: values.scale,
    };

    saveToDatabase(savedRecord);
  },
});

// On upload / first load
savedRecord.imageUrl = uploadedUrl;
savedRecord.imageType = file.type;

await cropper.setLego(savedRecord);

Optional: save flattened image

const dataUrl = cropper.getCanvasDataUrl();
await uploadToCdn(dataUrl);

File upload

const file = input.files[0];
const dataUrl = await readFileAsDataUrl(file);

await cropper.setLego({
  imageUrl: dataUrl,
  imageType: file.type,
  frameSize: { width: 600, height: 600 },
});

function readFileAsDataUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

React integration

import { useEffect, useRef } from "react";
import initLego from "lego-image";

export default function ImageEditor({ saved }) {
  const canvasRef = useRef(null);
  const cropperRef = useRef(null);

  useEffect(() => {
    if (!canvasRef.current) return;

    cropperRef.current = initLego({
      canvas: canvasRef.current,
      onChange: (values) => {
        saveToApi({
          imageUrl: saved?.imageUrl,
          imageType: saved?.imageType,
          frameSize: cropperRef.current.getFrameSize(),
          transform: {
            x: values.x,
            y: values.y,
            width: values.width,
            height: values.height,
            rotate: values.rotate,
            scale: values.scale,
          },
        });
      },
    });

    if (saved?.imageUrl) {
      cropperRef.current.setLego(saved);
    }

    return () => cropperRef.current?.destroy?.();
  }, []);

  return (
    <div style={{ width: 600, height: 600, overflow: "hidden" }}>
      <canvas ref={canvasRef} />
    </div>
  );
}

Vanilla HTML example

<div id="frame" style="width:600px;height:600px;overflow:hidden">
  <canvas id="canvas"></canvas>
</div>

<script type="module">
  import initLego from "lego-image";

  const cropper = initLego({
    canvas: document.getElementById("canvas"),
    onChange: (v) => console.log(v),
  });

  await cropper.setLego({
    imageUrl: "https://example.com/photo.png",
    imageType: "image/png",
    frameSize: { width: 600, height: 600 },
  });
</script>

DOM preview (CSS)

Use the conversion helper above. x and y are visual coordinates — they match the canvas at any rotation.

Fixed layout (set once):

position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
object-fit: contain;

Only transform updates from onChange (plus width / height on zoom):

const px = transformToPixels(values, frameSize.width, frameSize.height);

element.style.width = `${px.width}px`;
element.style.height = `${px.height}px`;
element.style.transform =
  `translate(${px.left}px, ${px.top}px) rotate(${px.rotate}deg)`;

transform-origin is always 0 0 — do not calculate it from x / y. It is not part of the saved transform.

Full CSS example

.preview-image {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  object-fit: contain;
  will-change: transform;
}
function applyDomPreview(element, values, frameW, frameH) {
  const px = transformToPixels(values, frameW, frameH);
  element.style.width = `${px.width}px`;
  element.style.height = `${px.height}px`;
  element.style.transform =
    `translate(${px.left}px, ${px.top}px) rotate(${px.rotate}deg)`;
}

The canvas export (getCanvasDataUrl) is the source of truth for the final cropped image. The DOM preview mirrors position/rotation for UI only.


Full API summary

const cropper = initLego({ canvas, onChange });

await cropper.setLego({ imageUrl, imageType, frameSize, transform });
await cropper.setFrameSize(width, height);
cropper.getFrameSize();

cropper.zoomIn();
cropper.zoomOut();
cropper.zoomToScale(1.4);
cropper.getZoomLimits();
cropper.rotateTo(45);
cropper.rotateBy(15);
cropper.rotateRight();
cropper.rotateLeft();
cropper.resetImage();

cropper.getCanvasDataUrl();
cropper.getSourceMimeType();
cropper.getExportMimeType();

Tips

| Topic | Recommendation | |-------|----------------| | Data format | Always use { imageUrl, imageType, frameSize, transform } | | First load | Omit transform for default cover fit | | Restore | Pass full saved record to setLego — visual x / y are converted automatically | | CSS preview | left: 0; top: 0; transform-origin: 0 0; convert fractions to px with transformToPixels() | | PNG transparency | Use getCanvasDataUrl() without forcing JPEG | | React | Do not set width/height on <canvas> element |


License

MIT