ScreenshotNeo

BlogHow-to

How to Generate Images Dynamically with JavaScript

Learn to draw, composite, and export raster images in the browser with Canvas, including CORS, performance, accessibility, and downloads.

By the ScreenshotNeo team29 September 202610 min read

How to Generate Images Dynamically with JavaScript

To generate an image dynamically in browser JavaScript, draw into a <canvas>, then export the canvas as a PNG, JPEG, or WebP file. Set the canvas’s intrinsic pixel dimensions, obtain a 2D context, draw shapes or loaded image sources, and call toBlob() when you need a downloadable or uploadable result.

This guide covers procedural drawing, image compositing, cropping, resizing, downloads, cross-origin images, performance, accessibility, and failure handling. It focuses on browser-side Canvas APIs. Server-side image pipelines and AI image-generation services are separate approaches.

1. The basic Canvas workflow

A canvas has two kinds of dimensions:

Canvas combines drawing commands and loaded sources before exporting a bitmap.
Canvas combines drawing commands and loaded sources before exporting a bitmap.
  • Intrinsic dimensions: the actual bitmap width and height in pixels.
  • CSS dimensions: how large that bitmap appears on the page.

Set the intrinsic dimensions for the output you want. Changing only CSS dimensions scales the existing bitmap and can make the result blurry or distorted. The following complete page draws a background, a card, text, and a circle, then creates a download link.

<!doctype html>
<html lang='en'>
<meta charset='utf-8'>
<title>Canvas image generator</title>
<style>
  canvas { max-width: 100%; border: 1px solid #ccc; }
</style>
<canvas id='art' width='1200' height='630'>
  Your browser does not support canvas output.
</canvas>
<button id='save' type='button'>Download image</button>
<script>
  const canvas = document.querySelector('#art');
  const ctx = canvas.getContext('2d');

  if (!ctx) throw new Error('This browser does not provide a 2D canvas context');

  // Background.
  ctx.fillStyle = '#f4f7fb';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Card.
  ctx.fillStyle = '#ffffff';
  ctx.roundRect(70, 70, 1060, 490, 28);
  ctx.fill();

  // Decorative circle.
  ctx.fillStyle = '#635bff';
  ctx.beginPath();
  ctx.arc(930, 190, 90, 0, Math.PI * 2);
  ctx.fill();

  // Text.
  ctx.fillStyle = '#172033';
  ctx.font = '700 54px system-ui, sans-serif';
  ctx.fillText('Dynamic image', 140, 220);
  ctx.font = '400 30px system-ui, sans-serif';
  ctx.fillStyle = '#526078';
  ctx.fillText('Generated with the Canvas 2D API', 140, 280);

  document.querySelector('#save').addEventListener('click', () => {
    canvas.toBlob((blob) => {
      if (!blob) {
        console.error('The browser could not encode this canvas');
        return;
      }
      const objectUrl = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = objectUrl;
      link.download = 'dynamic-image.png';
      link.click();
      URL.revokeObjectURL(objectUrl);
    }, 'image/png');
  });
</script>
</html>

The 2D context provides drawing methods such as fillRect(), paths, gradients, text, transformations, clipping, and compositing. Canvas can also draw an HTMLImageElement, SVG image, video frame, another canvas, ImageBitmap, OffscreenCanvas, or VideoFrame. See the MDN drawImage reference for source and cropping behavior.

2. Generate a reusable image function

For applications such as avatars, social cards, invoices, or badges, put the drawing code in a function that accepts data and returns a canvas. Keep dimensions in one place so the same design can be rendered at different sizes.

function makeBadge({ title, subtitle, color = '#2563eb' }) {
  const canvas = document.createElement('canvas');
  canvas.width = 1000;
  canvas.height = 600;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('2D context unavailable');

  ctx.fillStyle = '#0f172a';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = color;
  ctx.fillRect(0, 0, 26, canvas.height);

  ctx.fillStyle = '#ffffff';
  ctx.font = '700 64px system-ui, sans-serif';
  ctx.fillText(title, 90, 240);

  ctx.fillStyle = '#cbd5e1';
  ctx.font = '400 32px system-ui, sans-serif';
  ctx.fillText(subtitle, 90, 310);

  return canvas;
}

const badge = makeBadge({
  title: 'Release ready',
  subtitle: 'Generated in the browser',
  color: '#16a34a'
});
document.body.append(badge);

Use devicePixelRatio when you want a sharp display result on high-density screens. Set the backing bitmap to the CSS size multiplied by the device-pixel ratio, then scale the context before drawing:

function setupHiDpiCanvas(canvas, cssWidth, cssHeight) {
  const ratio = window.devicePixelRatio || 1;
  canvas.width = Math.round(cssWidth * ratio);
  canvas.height = Math.round(cssHeight * ratio);
  canvas.style.width = `${cssWidth}px`;
  canvas.style.height = `${cssHeight}px`;
  const ctx = canvas.getContext('2d');
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
  return ctx;
}

3. Composite a photo or other image source

Wait until an image has loaded before calling drawImage(). Register the load and error handlers before assigning src, because cached images can complete quickly.

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.addEventListener('load', () => resolve(image));
    image.addEventListener('error', () => reject(new Error(`Could not load ${url}`)));
    image.src = url;
  });
}

async function compose() {
  const source = await loadImage('/assets/photo.jpg');
  const canvas = document.querySelector('#composition');
  canvas.width = 1200;
  canvas.height = 800;
  const ctx = canvas.getContext('2d');

  ctx.fillStyle = '#111827';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the full source image into a 900 x 600 area.
  ctx.drawImage(source, 150, 100, 900, 600);

  // Add a translucent overlay and label.
  ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
  ctx.fillRect(150, 570, 900, 130);
  ctx.fillStyle = '#fff';
  ctx.font = '700 42px system-ui, sans-serif';
  ctx.fillText('A dynamic composition', 200, 650);
}

compose().catch(console.error);

drawImage(source, dx, dy, dWidth, dHeight) scales the complete source. To crop a source, use the nine-argument form: drawImage(source, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight). This is useful for thumbnails, avatars, and cover images.

4. Handle cross-origin images and the tainted canvas error

A canvas becomes tainted when you draw an image from another origin without a permitted CORS response. A tainted canvas can still be displayed, but pixel reads and export methods such as toBlob() and toDataURL() fail for security reasons.

Cross-origin images need CORS permission before a canvas can be exported.
Cross-origin images need CORS permission before a canvas can be exported.

Set crossOrigin before assigning src, and make sure the image host sends an appropriate Access-Control-Allow-Origin response:

function loadCorsImage(url) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.crossOrigin = 'anonymous';
    image.addEventListener('load', () => resolve(image));
    image.addEventListener('error', () => reject(new Error('Image or CORS request failed')));
    image.src = url;
  });
}

If the remote server does not allow your origin, setting crossOrigin cannot fix the problem. Use a same-origin copy, configure the asset server, or fetch and proxy the asset through a server you control while respecting its access rules. Do not treat a browser-side workaround as a way to bypass CORS.

5. Export PNG, JPEG, and WebP

Prefer canvas.toBlob(callback, type, quality) for downloads, uploads, and larger images. PNG is the default and required format. JPEG and WebP may be supported; their quality argument is a number from 0 to 1 and applies to lossy formats. Unsupported types fall back to PNG.

function canvasToBlob(canvas, type = 'image/png', quality) {
  return new Promise((resolve, reject) => {
    canvas.toBlob((blob) => {
      if (blob) resolve(blob);
      else reject(new Error('Canvas encoding returned no Blob'));
    }, type, quality);
  });
}

async function downloadCanvas(canvas, filename, type = 'image/webp', quality = 0.85) {
  const blob = await canvasToBlob(canvas, type, quality);
  const objectUrl = URL.createObjectURL(blob);
  try {
    const link = document.createElement('a');
    link.href = objectUrl;
    link.download = filename;
    link.click();
  } finally {
    URL.revokeObjectURL(objectUrl);
  }
}

downloadCanvas(document.querySelector('#art'), 'art.webp').catch(console.error);

Use toDataURL() only when an inline data URL is specifically required. It encodes the entire image into a JavaScript string, which increases memory use and can exceed URL-length limits for large results. Object URLs point to a Blob without copying the encoded bytes into a long URL string. Revoke each object URL after the image, preview, or download no longer needs it.

6. Choose the right output and drawing approach

Need Recommended approach Reason
Photo-like raster output Canvas 2D with JPEG or WebP Controls dimensions, cropping, and lossy quality.
Lossless graphics or transparency Canvas 2D with PNG Preserves alpha and avoids lossy compression.
Resolution-independent shapes or markup-controlled artwork Consider inline SVG Vector output can remain sharp at different sizes.
Complex work that blocks interaction OffscreenCanvas in a worker Moves rendering away from the document’s main thread.
Many independent layers Separate canvases or a scene model Makes selective redraws and updates easier.

Canvas is a bitmap surface. It does not retain semantic descriptions of the objects you draw. Provide alternative text or an equivalent HTML representation for meaningful images, and make controls such as Save and Regenerate keyboard accessible.

7. Resize and crop without distortion

Preserve the source aspect ratio unless intentional stretching is part of the design. A contain calculation fits the complete image inside a box; a cover calculation fills the box and crops overflow.

function drawCover(ctx, image, x, y, width, height) {
  const scale = Math.max(width / image.naturalWidth, height / image.naturalHeight);
  const sourceWidth = width / scale;
  const sourceHeight = height / scale;
  const sourceX = (image.naturalWidth - sourceWidth) / 2;
  const sourceY = (image.naturalHeight - sourceHeight) / 2;

  ctx.drawImage(
    image,
    sourceX, sourceY, sourceWidth, sourceHeight,
    x, y, width, height
  );
}

Large bitmaps consume memory approximately in proportion to width × height × four bytes for an uncompressed RGBA surface, plus temporary buffers used during decoding and export. Set practical maximum dimensions, reject unexpectedly huge uploads, and release unused canvases and object URLs.

8. Reliability and performance checklist

  • Set intrinsic dimensions before drawing; clear and redraw when dimensions change.
  • Load every image before compositing and handle its error event.
  • Use CORS-enabled sources when the result must be read or exported.
  • Reuse a canvas for repeated frames instead of creating one per update.
  • Batch drawing operations and avoid unnecessary full-canvas redraws.
  • Move expensive rendering to OffscreenCanvas and a worker when the main thread becomes unresponsive.
  • Use toBlob() for output, and choose PNG, JPEG, or WebP based on transparency and quality needs.
  • Catch null Blob results and encoding exceptions.
  • Revoke object URLs after use.
  • Show a progress state when loading several images or exporting a large bitmap.

9. Troubleshooting common errors

Symptom Likely cause Fix
Canvas is blank Drawing ran before the image loaded, or the context is null. Await the image load promise and check getContext('2d').
SecurityError during export A cross-origin source tainted the canvas. Set crossOrigin before src and configure CORS on the source server.
Export looks blurry CSS resized a low-resolution bitmap, or device-pixel ratio was ignored. Set intrinsic dimensions to the target pixels and use a high-DPI setup.
JPEG has a black or unexpected background JPEG has no alpha channel. Paint an explicit background before exporting JPEG, or use PNG/WebP for transparency.
Download does nothing The download was not initiated from a user gesture, or the object URL was revoked too early. Call the download from a click handler and revoke the URL after the click or image use.
Export returns PNG despite requesting WebP The requested format is unsupported. Inspect blob.type and provide a PNG fallback.
Page freezes during rendering The main thread is processing a large bitmap or many layers. Reduce dimensions, render incrementally, or use OffscreenCanvas in a worker.

10. Or skip the browser setup

If your actual goal is a screenshot of a rendered webpage rather than a procedurally drawn bitmap, ScreenshotNeo provides a website screenshot API. The request returns PNG, JPEG, WebP, or PDF, and the API accepts options for full-page capture, element selectors, dark mode, device presets, custom CSS and JavaScript, waits, headers, cookies, blocking, resizing, caching, signed links, asynchronous jobs, and bulk capture. See the ScreenshotNeo documentation for the complete parameter list.

One GET request is enough:

curl -G 'https://api.screenshotneo.com/v1/shot' \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    'https://api.screenshotneo.com/v1/shot',
    params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'},
    timeout=90,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.

11. Frequently asked questions

Can JavaScript generate an image without a server?

Yes. Canvas drawing and export run in the browser. A server is only needed if you must store, transform, distribute, or process the result centrally.

Why must an image load before drawImage?

drawImage() needs decoded source pixels. Calling it before the load event can produce an empty or incomplete result.

Which format should I use?

Use PNG for lossless output and transparency. Use JPEG for smaller photographic files without transparency. Use WebP when supported and you want a modern lossy or lossless web format.

Can I export a canvas containing a remote image?

Only when the remote image is permitted by CORS. Otherwise the canvas becomes tainted and browser security prevents pixel reads and export.

When should I choose SVG instead?

Choose SVG when your artwork is primarily resolution-independent shapes or markup-controlled elements and you need vector scaling. Choose Canvas when you need a raster bitmap, pixel compositing, or canvas-native drawing.

How do I keep a generated image accessible?

Give the resulting image meaningful alternative text, expose keyboard-accessible controls, and provide an equivalent textual or HTML representation when the visual conveys essential information.