LuminaJS is a modular, chainable, lightweight, zero-dependency browser-first JavaScript utility library for image processing with the HTML5 Canvas and ImageData APIs.
Scope: LuminaJS is for browser/client runtimes. It is not a universal JS image processor and is not designed for Node.js server-side image pipelines.
For server-side image processing, use tools like Sharp, Jimp, or ImageMagick.
- LuminaJS is: a browser-first Canvas +
ImageDatalibrary for interactive client-side image workflows. - LuminaJS is: optimized for modern ESM bundlers with modular imports.
- LuminaJS is not: a Node.js/server-side image processing engine.
- LuminaJS is not: a GPU shader/WebGL pipeline; pixel filters run in JavaScript on
ImageData.
- Demo-react
- Demo-react-storybook
- Demo-vanila-js
- Demo-Lumina-CSS
- Code
- NPM
- Documentation
- Lumina Image CSS Guide
- Performance Guide
If you do not need JavaScript image processing and only want to style, position, animate, or visually transform images with CSS, use Lumina Image CSS. It is designed for non-destructive image presentation with utility classes for filters, hover effects, overlays, layout, and responsive media frames.
- Responsive Browser Workflows: Canvas rendering can be browser-optimized; pixel filters run in JavaScript on
ImageData. - 🧩 Modular: Only import the filters and utilities you need.
- 🖼️ Canvas-Powered: Leverages the HTML5 Canvas API for seamless browser integration.
- 📦 Lightweight: Zero external dependencies (no jQuery, no Lodash).
- Heavy filters (
blur,gaussianBlur,backgroundBlur, convolution-based filters) can block the main thread on large images. - Resize first for previews/interactions, then process full resolution only for final export.
- Use Web Workers for expensive operations to keep UI interactions responsive.
- See Performance Guide for benchmark harness and worker examples.
While libraries like Jimp are built for server-side processing, LuminaJS is focused on browser image workflows: previews, crop/resize flows, filters, and export before upload. It trades server-runtime breadth for a small, client-side API.
- Low-Latency UX: Canvas rendering/compositing paths can be browser-optimized, while filters run in JavaScript over
ImageData. - Small Client Footprint: Zero runtime dependencies, ESM subpath exports, and separately shipped CSS/React entry points help keep app bundles focused on what you import.
- Privacy-Centric: All processing happens on the client’s machine. Sensitive user data never leaves the browser.
- Modern DX: Native TypeScript support and a clean, chainable API designed for ESM workflows.
| Scenario | LuminaJS | Jimp/Other |
|---|---|---|
| Interactive UI | Best. Real-time filters & previews. | Slow; often requires loaders. |
| Edge Cases | Best. Works in Web Workers. | High memory overhead. |
| Marketing Tools | Best. Dynamic watermarks & social overlays. | Overkill; increases bounce rates. |
| Server-Side | Out of scope (use Sharp/Jimp/ImageMagick) | Best. Built for Node.js environments. |
| Feature | LuminaJS | Jimp/Other |
|---|---|---|
| Bundle Size | Small ESM core; CSS/React shipped separately | Often much larger |
| Execution | JS pixel filters on ImageData + Canvas rendering |
Mostly JS pipelines |
| Environment | Browser / OffscreenCanvas (client-side) | Node.js / Browser |
| Dependencies | Zero | Multiple |
| API Style | Chainable & Functional | Chainable |
Positioning: Use LuminaJS when you need browser-side image preview, crop, resize, filter, and export workflows without bringing a server-oriented image stack into your frontend bundle.
# npm
npm install @gks101/luminajs
# pnpm
pnpm add @gks101/luminajs
# yarn
yarn add @gks101/luminajsLuminaJS includes first-class React support via hooks and components. See the React Integration section for full examples.
SSR/Next.js: LuminaJS React components and hooks depend on browser APIs (
window,<canvas>,ImageData). Use them only on the client side ('use client',next/dynamic(..., { ssr: false }), or equivalent).
LuminaJS targets modern browsers with Canvas, ImageData, Blob, and ES module support. Test your exact image sizes on the devices you support.
| Environment | Status / Notes |
|---|---|
| Chrome / Edge / Firefox | Primary targets for Canvas + ImageData workflows. |
| Safari / iOS Safari | Supported for standard workflows; large images can hit memory limits sooner. |
| Web Workers | Recommended for expensive filters when you can pass ImageData to a worker. |
| Node.js / SSR render passes | Out of scope for processing; import-safe, but runtime image work is client-only. |
ImageCropper/ImageAreaSelectorsupport keyboard movement (arrow keys), larger movement (Shift + Arrow), and resize (Alt + Arrow) once the crop region exists.- Apply/Reset controls expose ARIA labels and can be themed via class/style hooks.
- Current limitation: drag-selection and resize handles are still pointer-first interactions. Screen-reader narration for geometric crop state (x/y/width/height) is limited.
- For strict WCAG workflows, pair crop interactions with explicit numeric inputs for crop coordinates and dimensions in your surrounding UI.
# npm
npm install @gks101/luminajs
# pnpm
pnpm add @gks101/luminajs
# yarn
yarn add @gks101/luminajsimport { useLumina, LuminaCanvas, ImageCropper } from '@gks101/luminajs/react';Need polished image UI without rewriting pixels? Use lumina-image.css for CSS-only image effects and non-destructive image styling (filters, hover effects, overlays, layout utilities).
CSS effects are presentation-only: they do not crop pixels, mutate source image data, or export transformed pixel output.
| Capability | Lumina Image CSS | LuminaJS (JS API) |
|---|---|---|
| Filters / hover effects / transforms | Yes | Yes |
| Overlays / layout utilities | Yes | Limited (handled in your UI/CSS) |
| Pixel crop/resize | No | Yes (crop, resize) |
| Blob/data URL export | No | Yes (toBlob, toDataURL) |
| Permanent pixel changes | No (non-destructive) | Yes (ImageData processing + export/render) |
import '@gks101/luminajs/lumina-image.css';- Stable subpath import is kept:
@gks101/luminajs/lumina-image.css. - Current distribution decision: keep Lumina Image CSS inside
@gks101/luminajs(single package install). - Future package decision: no standalone package yet. We may introduce
@gks101/lumina-image-csslater if CSS release cadence diverges or consumer demand justifies split packaging. - Tree-shaking behavior: CSS is intentionally marked as a side effect in
package.json("sideEffects": ["./dist/lumina-image.css"]) so bundlers do not drop it. - CSS size target: minified
dist/lumina-image.csstarget is <= 18 KB (enforced in build).
<!-- CDN (pin to a version in production) -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@gks101/luminajs@2.0.5-beta/dist/lumina-image.css"
/><!-- Local package path -->
<link
rel="stylesheet"
href="node_modules/@gks101/luminajs/dist/lumina-image.css"
/>If you want to generate the optimized distributable files locally:
# 1. Install dependencies
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# 2. Run the build command
npm run buildThe output will be generated in the dist/ directory.
The easiest way to use LuminaJS is via the fluent chainable API. It handles image loading, canvas management, and filter sequencing automatically.
import { lumina } from '@gks101/luminajs';
// Process an image from a URL, apply filters, and draw to a canvas
await lumina('photo.jpg')
.brightness(20)
.contrast(10)
.grayscale()
.sharpen()
.toCanvas(document.getElementById('myCanvas'));
// Quick display in an <img> tag using an ID
await lumina('photo.jpg').sepia().toHtmlElement('myImageElement');
// Or get the result as a Blob for uploading
const blob = await lumina(fileInput.files[0])
.resize(800, 600)
.sepia()
.toBlob('image/jpeg', 0.8);This is the recommended client workflow for upload forms: create a smaller interactive preview first, then generate the final blob when the user saves.
import { lumina } from '@gks101/luminajs';
const input = document.querySelector('#avatar-input');
const preview = document.querySelector('#avatar-preview');
const save = document.querySelector('#save-avatar');
let selectedFile;
input.addEventListener('change', async (event) => {
selectedFile = event.target.files?.[0];
if (!selectedFile) return;
await lumina(selectedFile)
.resize(360, 360)
.brightness(8)
.contrast(6)
.toCanvas(preview);
});
save.addEventListener('click', async () => {
if (!selectedFile) return;
const blob = await lumina(selectedFile)
.resize(800, 800)
.crop(0, 0, 800, 800)
.sharpen()
.toBlob('image/jpeg', 0.86);
const body = new FormData();
body.append('avatar', blob, 'avatar.jpg');
await fetch('/api/avatar', { method: 'POST', body });
});import { loadImage, grayscale } from '@gks101/luminajs';<script type="module">
import { lumina } from '/node_modules/@gks101/luminajs/dist/lumina.min.js';
await lumina('photo.jpg')
.grayscale()
.toCanvas(document.getElementById('preview'));
</script>lumina(source): Initiates a processing chain.sourcecan be a URL string,Fileobject,HTMLImageElement,HTMLCanvasElement, orImageData..grayscale(): Applies grayscale..brightness(level): Adjusts brightness..contrast(level): Adjusts contrast..sepia(): Applies sepia..blur(radius): Applies box blur..gaussianBlur(sigma): Applies Gaussian blur..watermark(text, options): Adds text watermark..sharpen()/.emboss()/.edgeDetection(): Convolution filters..resize(w, h)/.crop(x, y, w, h): Transformations..render(): ReturnsPromise<ImageData>..toCanvas(canvas): Draws to canvas and returnsPromise<HTMLCanvasElement>..toHtmlElement(elementOrId): Displays result in an<img>(src) or<canvas>element..toBlob(mime, quality): ReturnsPromise<Blob>..toDataURL(mime, quality): ReturnsPromise<string>.
loadImage(source): Returns aPromiseresolving to anHTMLImageElement. Supports URL strings andFileobjects.getPixelData(image): ExtractsImageDatafrom an image using an offscreen canvas.putPixelData(canvas, imageData): WritesImageDataback to a canvas element.canvasToBlob(canvas, mimeType, quality): Async conversion of a canvas to aBlob.resize(source, width, height): Resizes an image or canvas. Returns a newHTMLCanvasElement.crop(source, x, y, width, height): Crops an image or canvas. Returns a newHTMLCanvasElement.
grayscale(imageData): Converts image to grayscale using ITU-R BT.601.brightness(imageData, level): Adjusts brightness [-255, 255].contrast(imageData, level): Adjusts contrast [-100, 100].sepia(imageData): Applies a classic antique sepia tone.ascii(imageData, options): Transforms an image into an ASCII text string. Recommended to use withgetResizedImageData.blur(imageData, radius): Applies a box blur effect.radiusis the blur intensity (default: 1).gaussianBlur(imageData, sigma): Applies a smooth Gaussian blur effect.sigmais the standard deviation (default: 2).watermark(imageData, text, options): Overlays text on the image. Options includex,y,font,color.backgroundBlur(imageData, options): Selectively blurs the background. Options includesigma,centerX,centerY,focusRadius,falloff.applyConvolution(data, width, height, kernel): Generic convolution engine for custom matrix operations (e.g., 3x3 kernel).sharpen(imageData): Sharpens the image using a convolution kernel.emboss(imageData): Applies an emboss effect using a convolution kernel.edgeDetection(imageData): Highlights edges using a convolution kernel.
import { lumina } from '@gks101/luminajs';
// 100 characters wide, auto-calculated height
const text = await lumina('photo.jpg').resize(100, 50).ascii().render();
console.log(text);import { loadImage, getResizedImageData, ascii } from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
// 1. Downsample for text (e.g., 100 characters wide)
const smallData = getResizedImageData(img, 100, 50);
// 2. Convert to ASCII
const text = ascii(smallData);
// 3. Display
console.log(text);import { lumina } from '@gks101/luminajs';
// Resize and then crop in one go
await lumina('photo.jpg')
.resize(800, 600)
.crop(100, 100, 300, 300)
.toCanvas(document.getElementById('myCanvas'));import { loadImage, resize, crop } from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
// 1. Resize to 800x600
const resizedCanvas = resize(img, 800, 600);
// 2. Crop a 300x300 square from (100, 100)
const croppedCanvas = crop(resizedCanvas, 100, 100, 300, 300);
document.body.appendChild(croppedCanvas);import { loadImage, getPixelData, putPixelData, blur } from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
const { imageData } = getPixelData(img);
// Apply blur with radius 5
const blurredData = blur(imageData, 5);
// Render back to canvas
const canvas = document.getElementById('myCanvas');
putPixelData(canvas, blurredData);import {
loadImage,
getPixelData,
putPixelData,
gaussianBlur,
} from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
const { imageData } = getPixelData(img);
// Apply smooth Gaussian blur with sigma 3.5
const blurredData = gaussianBlur(imageData, 3.5);
// Render back to canvas
const canvas = document.getElementById('myCanvas');
putPixelData(canvas, blurredData);import {
loadImage,
getPixelData,
putPixelData,
watermark,
} from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
const { imageData } = getPixelData(img);
// Add a semi-transparent watermark at (20, 20)
const watermarkedData = watermark(imageData, '© 2024 LuminaJS', {
x: 20,
y: 20,
font: '32px Arial',
color: 'rgba(255, 255, 255, 0.5)',
});
const canvas = document.getElementById('myCanvas');
putPixelData(canvas, watermarkedData);import {
loadImage,
getPixelData,
putPixelData,
backgroundBlur,
} from '@gks101/luminajs';
const img = await loadImage('portrait.jpg');
const { imageData } = getPixelData(img);
// Apply a portrait blur effect (sharp center, blurred background)
const portraitData = backgroundBlur(imageData, {
sigma: 6,
focusRadius: 150,
falloff: 200,
});
const canvas = document.getElementById('myCanvas');
putPixelData(canvas, portraitData);LuminaJS provides a generic convolution engine (applyConvolution) along with pre-built convolution filters such as sharpen, emboss, and edgeDetection. These filters modify pixels based on the values of their neighbors using a 3x3 matrix.
import {
loadImage,
getPixelData,
putPixelData,
sharpen,
emboss,
edgeDetection,
} from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
const { imageData } = getPixelData(img);
// Apply a built-in sharpen filter
const sharpenedData = sharpen(imageData);
// Or apply emboss or edge detection
// const embossedData = emboss(imageData);
// const edgeData = edgeDetection(imageData);
const canvas = document.getElementById('myCanvas');
putPixelData(canvas, sharpenedData);You can also pass your own custom 3x3 kernel (as an array of 9 numbers) to applyConvolution.
import {
loadImage,
getPixelData,
putPixelData,
applyConvolution,
} from '@gks101/luminajs';
const img = await loadImage('photo.jpg');
const { imageData } = getPixelData(img);
// Define a custom 3x3 kernel (e.g., an exaggerated edge detection kernel)
const customKernel = [-1, -1, -1, -1, 9, -1, -1, -1, -1];
// applyConvolution mutates the array data in place
applyConvolution(
imageData.data,
imageData.width,
imageData.height,
customKernel,
);
const canvas = document.getElementById('myCanvas');
putPixelData(canvas, imageData);LuminaJS provides a dedicated React entry point with hooks and components.
Client-only warning:
useLumina,LuminaCanvas,ImageAreaSelector, andImageCropperare browser-side features. Do not run them during server rendering.
The useLumina hook manages the image processing lifecycle, providing result, loading, and error states. You can pass image editing props directly, or use the operations function for advanced chaining. It also returns a getImage() function which you can call to generate the image on demand.
import { useLumina } from '@gks101/luminajs/react';
function ImagePreview({ file }) {
const { result, loading, error, getImage } = useLumina({
source: file,
grayscale: true,
brightness: 20,
sharpen: true,
outputType: 'dataUrl', // 'imageData' | 'dataUrl' | 'blob'
deps: [file],
});
const handleUpload = async () => {
// Generate the blob on demand
const blob = await getImage('blob');
// await myUploadFunction(blob);
};
if (loading) return <div>Processing image...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<img src={result} alt="Processed preview" />
<button onClick={handleUpload}>Upload Processed Image</button>
</div>
);
}A declarative way to render processed images directly onto a canvas element. It accepts image editing options directly as props!
import { LuminaCanvas } from '@gks101/luminajs/react';
function App() {
const handleImageGenerated = (dataUrl) => {
console.log('Got the generated image data URL:', dataUrl);
};
return (
<LuminaCanvas
source="portrait.jpg"
gaussianBlur={3}
sepia={true}
resize={{ width: 800, height: 600 }}
width={800} // HTML attribute
height={600} // HTML attribute
className="my-custom-canvas"
outputType="dataUrl"
getImage={handleImageGenerated}
onProcessError={(err) => console.error(err)}
/>
);
}ImageCropper provides a complete drag-to-crop workflow. Draw a crop area, drag inside it to move it, or drag any handle to resize it before clicking Apply Crop. The same component swaps from the original image selector to the cropped LuminaCanvas result. The Reset button reloads the original image for another crop. It natively supports mobile touch interactions, including pinch-to-resize for two-finger gestures.
import { useState } from 'react';
import { ImageCropper } from '@gks101/luminajs/react';
function AvatarEditor() {
const [avatarPreview, setAvatarPreview] = useState('');
return (
<>
<ImageCropper
src="portrait.jpg"
aspectRatio={1}
outputFormat="dataUrl"
maxWidth={500}
maxHeight={500}
allowResize={true}
onCropComplete={(result) => {
if (typeof result === 'string') setAvatarPreview(result);
}}
onError={(error) => console.error(error.message)}
/>
{avatarPreview && <img src={avatarPreview} alt="Cropped avatar" />}
</>
);
}For uploads, use outputFormat="blob":
<ImageCropper
src={file}
aspectRatio={16 / 9}
outputFormat="blob"
onCropComplete={async (result) => {
if (!(result instanceof Blob)) return;
const body = new FormData();
body.append('image', result, 'banner.png');
await fetch('/api/upload', { method: 'POST', body });
}}
/>Both useLumina and LuminaCanvas accept these explicit props to make image processing incredibly simple without writing chained operations manually:
| Prop | Type | Description |
|---|---|---|
grayscale |
boolean |
Applies a grayscale filter. |
brightness |
number |
Adjusts brightness [-255, 255]. |
contrast |
number |
Adjusts contrast [-100, 100]. |
sepia |
boolean |
Applies a classic antique sepia tone. |
blur |
number |
Applies a box blur effect. |
gaussianBlur |
number |
Applies a smooth Gaussian blur effect. |
sharpen |
boolean |
Sharpens the image. |
emboss |
boolean |
Applies an emboss effect. |
edgeDetection |
boolean |
Highlights edges. |
resize |
{ width: number, height: number } |
Resizes the image to the specified dimensions. |
crop |
{ x: number, y: number, width: number, height: number } |
Crops the image. |
watermark |
{ text: string, options?: any } |
Overlays text on the image. |
backgroundBlur |
any |
Selectively blurs the background. |
ascii |
boolean | Record<string, any> |
Transforms the image into ASCII text. |
A common requirement is to retrieve the processed image data so you can upload it to a server or pass it to another component. Both the hook and the component provide an easy way to do this via getImage.
The hook returns a getImage asynchronous function. This allows you to generate the image on demand (e.g. when a user clicks a "Save" button), using the latest props applied.
const handleUpload = async () => {
// You can override the outputType when calling getImage
const finalBlob = await getImage('blob');
await uploadToServer(finalBlob);
};The component accepts a getImage prop. This is a callback that will be triggered automatically as soon as the canvas finishes rendering the processed image.
<LuminaCanvas
source="photo.jpg"
outputType="dataUrl" // format passed to getImage (dataUrl, blob, imageData, canvas)
getImage={(dataUrl) => {
// Save to state, send to a parent component, etc.
setProcessedImage(dataUrl);
}}
/>This repository includes the ImageCropper component. New props were added to allow customizing the Apply Crop and Reset buttons:
- allowResize: boolean (optional) - Shows resize handles on the crop area so users can adjust an existing selection before applying. Default: true.
- applyButtonClassName: string (optional) - CSS class for the Apply button.
- applyButtonStyle: CSSProperties (optional) - Inline style object for the Apply button.
- resetButtonClassName: string (optional) - CSS class for the Reset button.
- resetButtonStyle: CSSProperties (optional) - Inline style object for the Reset button.
- showPreview: boolean (optional) - Shows the applied crop result inside the cropper after Apply. Default: true. Set false when the parent owns preview/upload UI.
- buttonPosition: 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-center' | 'bottom-right' (optional) - Position of the Apply/Reset button container. Default: 'top-left'.
- zIndex: number (internal) - The button container uses a high z-index (1001) to ensure the buttons render above the image selection overlay.
- onApply: (crop) => boolean | void | Promise<boolean | void> (optional) - Callback invoked when the Apply button is clicked. Returning
false(or a Promise resolving tofalse) will abort the component's default apply behavior. - onReset: () => boolean | void | Promise<boolean | void> (optional) - Callback invoked when Reset is clicked. Returning
falsewill abort the default reset.
These props are optional and backward-compatible.
MIT © LuminaJS Team