heygen-com/hyperframes · warning · Error

--ghost renders a canvas/WebGL motion trail, but this compos

Error message

--ghost renders a canvas/WebGL motion trail, but this composition has no <canvas>. Use the default --shot onion for DOM/SVG transform motion.

What it means

Thrown by captureGhostOnionSkin when --ghost is requested but the composition contains no <canvas> element. The ghost mode composites real painted canvas/WebGL pixels across sample times to produce a rendered motion trail; without a canvas there are no pixels to capture. DOM/SVG transform motion is already covered by the default marker onion-skin and does not need --ghost.

Source

Thrown at packages/cli/src/commands/motionShot.ts:526

// Rendered ("ghost") onion-skin: screenshot the REAL painted stage at each
// sample and composite them as translucent ghosts. This is the onion-skin for
// canvas/WebGL motion the marker sampler is blind to (the markers project a
// bbox; the pixels are the motion). Works for any visual composition, but
// requires a <canvas> (DOM/SVG transform motion already shows up in the
// default marker onion).
async function captureGhostOnionSkin(
  page: import("puppeteer-core").Page,
  requests: ShotRequest[],
  times: number[],
  size: FrameSize,
  camera: OrbitCamera,
  outPath: string,
): Promise<string> {
  await applyOrbitCameraIfAngled(page, requests, camera);
  const hasCanvas = await page.evaluate(() => document.querySelectorAll("canvas").length > 0);
  if (!hasCanvas) {
    throw new Error(
      "--ghost renders a canvas/WebGL motion trail, but this composition has no <canvas>. Use the default --shot onion for DOM/SVG transform motion.",
    );
  }
  const frames: string[] = [];
  for (const t of times) {
    frames.push(await captureGhostFrame(page, t));
  }
  const label = `${cameraLabel(camera)}  ·  rendered onion  ·  ${times.length} frames  ·  t ${times[0]}–${times[times.length - 1]}s`;
  const dataUrl = (await page.evaluate(
    compositeGhostFrames,
    frames,
    ghostAlphas(frames.length),
    size.width,
    size.height,
    label,
  )) as string;
  const b64 = String(dataUrl).replace(/^data:image\/png;base64,/, "");
  if (!b64) throw new Error("ghost composite returned no data");

View on GitHub (pinned to c2996c8626)

Solutions

  1. Drop --ghost to use the default marker onion-skin, which captures DOM/SVG transform motion
  2. Only use --ghost when the composition has a <canvas> (WebGL/2D canvas rendering)
  3. Verify document.querySelectorAll('canvas').length > 0 in the rendered page before using --ghost

Example fix

// before — DOM/SVG composition, no canvas
hyperframes shot --ghost out.png
// after — default marker onion for DOM/SVG motion
hyperframes shot out.png
Defensive patterns

Strategy: validation

Validate before calling

import { parseHTML } from 'linkedom';
function compositionHasCanvas(html: string): boolean {
  return parseHTML(html).document.querySelectorAll('canvas').length > 0;
}
// before running --ghost
if (!compositionHasCanvas(html)) {
  throw new Error('No <canvas> — use the default marker onion instead of --ghost');
}

Prevention

When it happens

Trigger: Running `hyperframes shot --ghost out.png` on a composition that animates only DOM elements (divs, SVG) with CSS/GSAP transforms and contains no <canvas>.

Common situations: Agents that default to --ghost for all shots. Users who assume --ghost improves any motion capture. Compositions that use WebGL conditionally (the canvas is added by JS that hasn't run).

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/cf8c82a2182fe8f5. Report an issue: GitHub.