heygen-com/hyperframes · error · Error

ghost composite returned no data

Error message

ghost composite returned no data

What it means

Thrown by captureGhostOnionSkin when the in-browser compositeGhostFrames function returned an empty string. This happens when the 2D context could not be acquired (getContext('2d') returned null), every frame's image failed to load, or toDataURL produced an empty result — often due to a cross-origin/tainted canvas that blocks pixel readback.

Source

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

    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");
  writeFileSync(outPath, Buffer.from(b64, "base64"));
  return outPath;
}

// Default (marker) onion-skin: seek to each sample time, read every element's
// projected corners. Marker children (zero-size) inherit the element's full
// transform chain, so their screen positions ARE the 3D projection of each
// corner — this is how 3D comes "for free" without an #stage assumption.
async function captureMarkerOnionSkin(
  page: import("puppeteer-core").Page,
  requests: ShotRequest[],
  times: number[],
  size: FrameSize,
  camera: OrbitCamera,
  frame: { layout: "path" | "strip"; fit: boolean; hasWindow: boolean },
  outPath: string,
): Promise<string> {
  // Orbit camera as its own step (keeps the sampler simple), only when angled.

View on GitHub (pinned to c2996c8626)

Solutions

  1. Ensure all images/textures drawn to canvas use CORS-enabled URLs (crossOrigin='anonymous') and serve proper headers
  2. Freeze remote assets locally with media-use so they are same-origin
  3. Retry the shot; transient GPU/context loss in headless Chrome can produce this
  4. Reduce the number of ghost samples if memory pressure is suspected

Example fix

// before — cross-origin image taints the canvas
const tex = new Image(); tex.src = 'https://cdn.example.com/tex.png';
// after — set crossOrigin before src, or use a local asset
tex.crossOrigin = 'anonymous';
tex.src = 'assets/tex.png';
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure all images drawn to canvas are same-origin or CORS-enabled:
const img = new Image();
img.crossOrigin = 'anonymous'; // set BEFORE src
img.src = 'assets/tex.png';   // local asset avoids taint entirely

Try / catch

try {
  await captureGhostOnionSkin(page, requests, times, size, camera, outPath);
} catch (e) {
  if (e instanceof Error && /ghost composite returned no data/.test(e.message)) {
    // check for cross-origin/tainted canvas, then retry or fall back to marker onion
  } else throw e;
}

Prevention

When it happens

Trigger: A canvas drawing cross-origin images without CORS headers, tainting the canvas so toDataURL throws or returns empty. Also when the composite canvas context is unavailable (headless GPU context lost).

Common situations: Compositions loading remote textures/images into WebGL without crossOrigin attributes. Headless Chrome GPU crashes that lose the 2D context. Very large frame sizes where allocation fails.

Related errors


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