Export to image
Export the whole graph — not just the viewport — to a Blob at any scale. Download PNG, retina 2×, or JPG straight from a panel.
import { useState } from 'react'
import { XenolithGraph, XenolithPanel, XenolithButton, XenolithControls, useEditor } from '@xenolithengine/graph-react'
import { loadDemo } from '@xenolithengine/demo/scene'
import { exportGraphImage } from '@xenolithengine/demo/export-image'
import { DemoStage } from '../Layout.js'
// Showcase: render the whole graph to an image. The export + download logic lives in the
// framework-agnostic core (@xenolithengine/demo/export-image); this React file is just the button panel.
function ExportPanel(): React.ReactElement {
const editor = useEditor()
const [busy, setBusy] = useState(false)
const exportAs = async (format: 'png' | 'jpeg', scale: number): Promise<void> => {
if (busy) return
setBusy(true)
try { await exportGraphImage(editor, format, scale) } finally { setBusy(false) }
}
const full = { width: '100%' }
return (
<XenolithPanel position="top-left" style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 190 }}>
<p style={{ margin: 0, fontSize: 11, textTransform: 'uppercase', letterSpacing: '.05em', color: 'var(--xeno-muted)' }}>Export image</p>
<XenolithButton style={full} disabled={busy} onClick={() => void exportAs('png', 1)}>↓ PNG · 1×</XenolithButton>
<XenolithButton style={full} disabled={busy} onClick={() => void exportAs('png', 2)}>↓ PNG · 2× (retina)</XenolithButton>
<XenolithButton style={full} disabled={busy} onClick={() => void exportAs('jpeg', 2)}>↓ JPG · 2×</XenolithButton>
<span style={{ color: 'var(--xeno-muted)', fontSize: 11, lineHeight: 1.45 }}>
Exports the entire graph, not just what’s on screen.
</span>
</XenolithPanel>
)
}
/** Showcase: full-graph image export at any scale. */
export function ExportImageDemo(): React.ReactElement {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={loadDemo}>
<XenolithControls position="bottom-left" />
<ExportPanel />
</XenolithGraph>
</DemoStage>
)
} // Render the whole graph to an image. editor.exportImage() draws every node (not just the visible
// viewport) into an offscreen canvas at any scale and returns a Blob — PNG for crisp UI, JPG for
// smaller files, 2× for retina. Framework-agnostic; the host just calls exportGraphImage from a button.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
/** Export the entire graph and trigger a download. */
export async function exportGraphImage(editor: XenolithEditor, format: 'png' | 'jpeg', scale: number): Promise<void> {
const blob = await editor.exportImage({ format, scale, padding: 48 })
download(blob, `graph@${scale}x.${format === 'jpeg' ? 'jpg' : 'png'}`)
} <script setup lang="ts">
// Vue — full-graph image export. The buttons live in a child so useEditor() injects
// inside <XenolithGraph>. Download logic stays in @xenolithengine/demo/export-image.
import { XenolithGraph, XenolithControls } from '@xenolithengine/graph-vue'
import { loadDemo } from '@xenolithengine/demo/scene'
import ExportPanel from './ExportPanel.vue'
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="loadDemo">
<XenolithControls position="bottom-left" />
<ExportPanel />
</XenolithGraph>
</div>
</template> <script setup lang="ts">
import { ref } from 'vue'
import { XenolithPanel, XenolithButton, useEditor } from '@xenolithengine/graph-vue'
import { exportGraphImage } from '@xenolithengine/demo/export-image'
const editor = useEditor()
const busy = ref(false)
async function save(format: 'png' | 'jpeg', scale: number): Promise<void> {
const e = editor.value
if (!e || busy.value) return
busy.value = true
try { await exportGraphImage(e, format, scale) } finally { busy.value = false }
}
</script>
<template>
<XenolithPanel position="top-left">
<div style="display:flex;flex-direction:column;gap:6px;width:190px;">
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted);">Export image</p>
<XenolithButton style="width:100%" :disabled="busy" @click="save('png', 1)">↓ PNG · 1×</XenolithButton>
<XenolithButton style="width:100%" :disabled="busy" @click="save('png', 2)">↓ PNG · 2× (retina)</XenolithButton>
<XenolithButton style="width:100%" :disabled="busy" @click="save('jpeg', 2)">↓ JPG · 2×</XenolithButton>
<span style="color:var(--xeno-muted);font-size:11px;line-height:1.45;">
Exports the entire graph, not just what’s on screen.
</span>
</div>
</XenolithPanel>
</template> // Render the whole graph to an image. editor.exportImage() draws every node (not just the visible
// viewport) into an offscreen canvas at any scale and returns a Blob — PNG for crisp UI, JPG for
// smaller files, 2× for retina. Framework-agnostic; the host just calls exportGraphImage from a button.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
/** Export the entire graph and trigger a download. */
export async function exportGraphImage(editor: XenolithEditor, format: 'png' | 'jpeg', scale: number): Promise<void> {
const blob = await editor.exportImage({ format, scale, padding: 48 })
download(blob, `graph@${scale}x.${format === 'jpeg' ? 'jpg' : 'png'}`)
} <!-- Svelte — full-graph image export. The editor from onready is enough; the download
helper is framework-agnostic. -->
<script lang="ts">
import { xenolith, createXenolithEditorContext } from '@xenolithengine/graph-svelte'
import { XenolithPanel, XenolithButton, XenolithControls } from '@xenolithengine/graph-svelte/components'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { loadDemo } from '@xenolithengine/demo/scene'
import { exportGraphImage } from '@xenolithengine/demo/export-image'
const panelEditor = createXenolithEditorContext()
let editor = $state<XenolithEditor | null>(null)
let busy = $state(false)
async function save(format: 'png' | 'jpeg', scale: number): Promise<void> {
if (!editor || busy) return
busy = true
try { await exportGraphImage(editor, format, scale) } finally { busy = false }
}
</script>
<div
use:xenolith={{ resizeToWindow: false }}
onready={(e) => { editor = e.detail; panelEditor.set(e.detail); loadDemo(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithControls position="bottom-left" />
<XenolithPanel position="top-left">
<div style="display:flex;flex-direction:column;gap:6px;width:190px;">
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted);">Export image</p>
<XenolithButton style="width:100%" disabled={busy} onclick={() => save('png', 1)}>↓ PNG · 1×</XenolithButton>
<XenolithButton style="width:100%" disabled={busy} onclick={() => save('png', 2)}>↓ PNG · 2× (retina)</XenolithButton>
<XenolithButton style="width:100%" disabled={busy} onclick={() => save('jpeg', 2)}>↓ JPG · 2×</XenolithButton>
<span style="color:var(--xeno-muted);font-size:11px;line-height:1.45;">
Exports the entire graph, not just what’s on screen.
</span>
</div>
</XenolithPanel> // Render the whole graph to an image. editor.exportImage() draws every node (not just the visible
// viewport) into an offscreen canvas at any scale and returns a Blob — PNG for crisp UI, JPG for
// smaller files, 2× for retina. Framework-agnostic; the host just calls exportGraphImage from a button.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
/** Export the entire graph and trigger a download. */
export async function exportGraphImage(editor: XenolithEditor, format: 'png' | 'jpeg', scale: number): Promise<void> {
const blob = await editor.exportImage({ format, scale, padding: 48 })
download(blob, `graph@${scale}x.${format === 'jpeg' ? 'jpg' : 'png'}`)
} // Solid — full-graph image export. The editor signal is filled from on:ready.
import { createSignal } from 'solid-js'
import { xenolith } from '@xenolithengine/graph-solid'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { loadDemo } from '@xenolithengine/demo/scene'
import { exportGraphImage } from '@xenolithengine/demo/export-image'
export function ExportImageDemo() {
const [editor, setEditor] = createSignal<XenolithEditor | null>(null)
const [busy, setBusy] = createSignal(false)
async function save(format: 'png' | 'jpeg', scale: number): Promise<void> {
const live = editor()
if (!live || busy()) return
setBusy(true)
try { await exportGraphImage(live, format, scale) } finally { setBusy(false) }
}
const btn = 'font:inherit;font-size:13px;padding:7px 12px;border-radius:8px;border:1px solid var(--xeno-border);background:var(--xeno-elevated);color:var(--xeno-text);width:100%;'
return (
<div
use:xenolith={{ resizeToWindow: false }}
on:ready={(e) => {
setEditor(e.detail)
e.detail.chrome.setControls({ position: 'bottom-left' })
loadDemo(e.detail)
}}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;left:12px;z-index:5;display:flex;flex-direction:column;gap:6px;width:190px;padding:10px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;color:var(--xeno-text);font:13px system-ui,sans-serif;">
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted);">Export image</p>
<button type="button" style={btn} disabled={busy()} onClick={() => void save('png', 1)}>↓ PNG · 1×</button>
<button type="button" style={btn} disabled={busy()} onClick={() => void save('png', 2)}>↓ PNG · 2× (retina)</button>
<button type="button" style={btn} disabled={busy()} onClick={() => void save('jpeg', 2)}>↓ JPG · 2×</button>
<span style="color:var(--xeno-muted);font-size:11px;line-height:1.45;">
Exports the entire graph, not just what’s on screen.
</span>
</div>
</div>
)
} // Render the whole graph to an image. editor.exportImage() draws every node (not just the visible
// viewport) into an offscreen canvas at any scale and returns a Blob — PNG for crisp UI, JPG for
// smaller files, 2× for retina. Framework-agnostic; the host just calls exportGraphImage from a button.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
/** Export the entire graph and trigger a download. */
export async function exportGraphImage(editor: XenolithEditor, format: 'png' | 'jpeg', scale: number): Promise<void> {
const blob = await editor.exportImage({ format, scale, padding: 48 })
download(blob, `graph@${scale}x.${format === 'jpeg' ? 'jpg' : 'png'}`)
} // Angular — full-graph image export. The click handler reads the service's editor.
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, signal, viewChild } from '@angular/core'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import { loadDemo } from '@xenolithengine/demo/scene'
import { exportGraphImage } from '@xenolithengine/demo/export-image'
@Component({
selector: 'export-image-demo',
standalone: true,
providers: [XenolithGraphService],
template: `
<div style="position:absolute;inset:0;">
<div #host class="xeno" style="position:absolute;inset:0;"></div>
<div class="card">
<p class="label">Export image</p>
<button type="button" class="btn" [disabled]="busy()" (click)="save('png', 1)">↓ PNG · 1×</button>
<button type="button" class="btn" [disabled]="busy()" (click)="save('png', 2)">↓ PNG · 2× (retina)</button>
<button type="button" class="btn" [disabled]="busy()" (click)="save('jpeg', 2)">↓ JPG · 2×</button>
<span class="note">Exports the entire graph, not just what’s on screen.</span>
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; left:12px; z-index:5; display:flex; flex-direction:column;
gap:6px; width:190px; padding:10px; background:var(--xeno-panel,#1d1d1d);
border:1px solid var(--xeno-border,#333); border-radius:10px;
color:var(--xeno-text,#cfcfcf); font:13px system-ui,sans-serif; }
.label { margin:0; font-size:11px; text-transform:uppercase; letter-spacing:.05em;
color:var(--xeno-muted,#9a9a9a); }
.btn { font:inherit; font-size:13px; padding:7px 12px; cursor:pointer; border-radius:8px;
border:1px solid var(--xeno-border,#333); background:var(--xeno-elevated,#262626);
color:var(--xeno-text,#cfcfcf); }
.btn:disabled { opacity:0.4; cursor:default; }
.note { color:var(--xeno-muted,#9a9a9a); font-size:11px; line-height:1.45; }
`],
})
export class ExportImageDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
busy = signal(false)
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, { resizeToWindow: false })
editor.chrome.setControls({ position: 'bottom-left' })
loadDemo(editor)
}
async save(format: 'png' | 'jpeg', scale: number): Promise<void> {
const editor = this.graph.editor
if (!editor || this.busy()) return
this.busy.set(true)
try { await exportGraphImage(editor, format, scale) } finally { this.busy.set(false) }
}
ngOnDestroy(): void { this.graph.destroy() }
} // Render the whole graph to an image. editor.exportImage() draws every node (not just the visible
// viewport) into an offscreen canvas at any scale and returns a Blob — PNG for crisp UI, JPG for
// smaller files, 2× for retina. Framework-agnostic; the host just calls exportGraphImage from a button.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
/** Export the entire graph and trigger a download. */
export async function exportGraphImage(editor: XenolithEditor, format: 'png' | 'jpeg', scale: number): Promise<void> {
const blob = await editor.exportImage({ format, scale, padding: 48 })
download(blob, `graph@${scale}x.${format === 'jpeg' ? 'jpg' : 'png'}`)
}