Skip to main content

Your first composition

This guide walks you through creating a simple image composition with layers, transforms, and export. The API is identical in Node.js and the browser.
1

Import and create a composition

Start by importing the Composition class and creating a new document:
import { Composition } from '@iamkaf/kimg';

const doc = await Composition.create({ width: 128, height: 128 });
The create method is async because it initializes the WASM module on first use.
2

Add an image layer

Add your first layer with RGBA pixel data:
// Assume rgbaPixels is a Uint8Array with 128*128*4 bytes
const layerId = doc.addImageLayer({
  name: 'sprite',
  rgba: rgbaPixels,
  width: 128,
  height: 128,
  x: 0,
  y: 0,
});
The method returns a numeric layer ID you can use to reference this layer.
3

Apply transforms

Apply non-destructive transforms to the layer:
doc.updateLayer(layerId, {
  opacity: 0.8,
  anchor: 'center',
  rotation: 22.5,
  scaleX: 1.25,
  scaleY: 0.75,
});
Transforms are non-destructive and cached for performance. You can change them at any time without re-rendering the original pixels.
4

Export to PNG

Render the composition and export as PNG:
const png = doc.exportPng();
The exportPng method returns a Uint8Array containing the PNG file data.

Complete example

Here’s a complete working example that creates a composition, adds layers, and exports:
import { Composition } from '@iamkaf/kimg';

// Create a 128×128 composition
const doc = await Composition.create({ width: 128, height: 128 });

// Add an image layer
const layerId = doc.addImageLayer({
  name: 'sprite',
  rgba: rgbaPixels, // Your RGBA pixel data
  width: 128,
  height: 128,
  x: 0,
  y: 0,
});

// Apply transforms
doc.updateLayer(layerId, {
  opacity: 0.8,
  anchor: 'center',
  rotation: 22.5,
  scaleX: 1.25,
  scaleY: 0.75,
});

// Export to PNG
const png = doc.exportPng();

// Create a download link
const blob = new Blob([png], { type: 'image/png' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'composition.png';
a.click();

Working with shape layers

Shape layers are vector-style primitives that render to pixels:
const doc = await Composition.create({ width: 200, height: 200 });

// Add a rounded rectangle badge
const badgeId = doc.addShapeLayer({
  name: "Badge",
  type: "roundedRect",
  x: 24,
  y: 24,
  width: 96,
  height: 40,
  radius: 12,
  fill: [255, 0, 0, 255], // Red
  stroke: { color: [255, 255, 255, 255], width: 2 }, // White border
});

const png = doc.exportPng();
Shape types include: rectangle, roundedRect, ellipse, line, and polygon.

Per-layer transforms

You can apply transforms to any layer type:
doc.updateLayer(layerId, {
  x: 10,
  y: -4,
  anchor: "center",
  flipX: false,
  flipY: true,
  rotation: 30,
  scaleX: 1.25,
  scaleY: 0.75,
});

Anchor points

  • topLeft or top_left — Transform around top-left corner (default)
  • center — Transform around center point

Bucket fill

Fill regions of a layer with a color:
// Bucket fill at layer-local coordinates (12, 18)
doc.bucketFillLayer(layerId, {
  x: 12,
  y: 18,
  color: [0, 255, 0, 255], // Green
  contiguous: true, // Only fill connected pixels
  tolerance: 0, // Exact color match
});
Tolerance is alpha-aware and checked per channel across RGBA. Higher values allow filling similar colors.

Importing images

kimg can import PNG, JPEG, WebP, and GIF files:
// Auto-detect format and import
const layerId = doc.importImage({
  name: 'photo',
  bytes: imageBytes, // Uint8Array of file contents
  x: 0,
  y: 0,
});

// Import GIF frames as separate layers
const frameIds = doc.importGifFrames({ bytes: gifBytes });
console.log(`Imported ${frameIds.length} frames`);

Exporting to different formats

Export your composition to PNG, JPEG, or WebP:
const png = doc.exportPng();

Layer management

Manage your layer stack programmatically:
// List all layers
const layers = doc.listLayers();
layers.forEach(layer => {
  console.log(`${layer.name}: ${layer.kind} at (${layer.x}, ${layer.y})`);
});

// Get specific layer info
const info = doc.getLayer(layerId);
console.log(info.name, info.visible, info.opacity);

// Remove a layer
doc.removeLayer(layerId);

// Count layers
const count = doc.layerCount();

Working with groups

Organize layers into groups:
// Create a group
const groupId = doc.addGroupLayer({ name: 'Effects' });

// Add layers to the group
const imageId = doc.addImageLayer({
  name: 'photo',
  rgba: pixels,
  width: 128,
  height: 128,
  parentId: groupId, // Add to group
});

// Add a filter layer to the group (affects all children)
const filterId = doc.addFilterLayer({
  name: 'color adjust',
  parentId: groupId,
});

doc.setFilterLayerConfig(filterId, {
  saturation: 0.5,
  brightness: 0.2,
});
Filter layers apply their effects to all layers below them in the same group, enabling scoped filter application.

Color utilities

kimg provides standalone color utilities:
import { hexToRgb, rgbToHex, relativeLuminance, contrastRatio } from '@iamkaf/kimg';

const rgb = await hexToRgb('#ff8000'); // Uint8Array [255, 128, 0]
const hex = await rgbToHex(255, 128, 0); // '#ff8000'

const luma = await relativeLuminance('#3b82f6'); // 0.2355 (WCAG 2.x)
const contrast = await contrastRatio('#ffffff', '#000000'); // 21.0

Memory management

kimg compositions are backed by WASM memory. Free them when done:
const doc = await Composition.create({ width: 128, height: 128 });

// ... use the composition

doc.free(); // Release WASM memory
If you’re using TypeScript with explicit resource management, compositions support Symbol.dispose:
using doc = await Composition.create({ width: 128, height: 128 });
// Automatically freed when out of scope

Next steps

Now that you’ve created your first composition, explore more features:

Blend modes

Learn about the 16 blend modes for layer compositing

Filters

Apply HSL adjustments, blur, sharpen, and more

Masks

Use layer masks and clipping masks for precise control

Sprite tools

Pack sprite sheets and create contact sheets

Build docs developers (and LLMs) love