lego-image
v2.0.2
Published
Canvas image cropper for JavaScript and React — pan, zoom, rotate, frame resize, export, and persist transform data.
Maintainers
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-imageContents
- Quick start
- Data format (canonical)
- Initialization
- API reference
- Transform coordinates
- Transform output (
onChange) - Converting transform values
- Save to database
- File upload
- React integration
- Vanilla HTML example
- DOM preview (CSS)
- Full API summary
- Tips
- License
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
transformfromonChange. Do not use deprecatedpos,scale, orrotate.
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 pointRestore
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
