Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
<span class="tb-stat" id="tb-velocity"><span class="tb-stat-icon tb-bolt">&#9889;</span><span class="tb-stat-val">0</span><span class="tb-stat-unit">/min</span></span>
</div>
<div class="tb-actions">
<button id="screenshot-btn" title="Screenshot" aria-label="Export screenshot">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 15.2a3.2 3.2 0 100-6.4 3.2 3.2 0 000 6.4z"/><path d="M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/></svg>
</button>
<button id="notif-btn" title="Notifications (N)" aria-label="Notifications" style="position:relative">&#128276;</button>
<button id="cmd-hint" title="Command Palette (Ctrl+K)" aria-label="Command palette">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Sidebar } from './ui/sidebar.js';
import { ToastManager } from './ui/toast-manager.js';
import { ShortcutsHelp } from './ui/shortcuts-help.js';
import { SessionExport } from './ui/session-export.js';
import { CanvasExport } from './ui/canvas-export.js';
import { Onboarding } from './ui/onboarding.js';
import { ZONE_MAP, AGENT_PALETTES } from '@agent-move/shared';

Expand Down Expand Up @@ -186,6 +187,9 @@ async function main() {
// ── Session Export ──
const sessionExport = new SessionExport(store);

// ── Canvas Screenshot Export ──
const canvasExport = new CanvasExport(pixiApp, world);

// ── Onboarding ──
const onboarding = new Onboarding();

Expand Down Expand Up @@ -554,6 +558,7 @@ async function main() {
sessionDetailPanel.dispose();
sessionComparisonPanel.dispose();
minimap.dispose();
canvasExport.dispose();
store.dispose();
});

Expand Down
97 changes: 97 additions & 0 deletions packages/client/src/ui/canvas-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Application } from 'pixi.js';
import type { WorldManager } from '../world/world-manager.js';

/**
* Canvas Screenshot Export — captures the full world view (all 9 zones)
* as a PNG and triggers an immediate download.
*
* Uses Pixi v8's renderer.extract API to render world.root to a canvas,
* temporarily hiding the day/night overlay for clarity.
*/
export class CanvasExport {
private app: Application;
private world: WorldManager;
private btn: HTMLButtonElement | null = null;
private capturing = false;

constructor(app: Application, world: WorldManager) {
this.app = app;
this.world = world;

this.btn = document.getElementById('screenshot-btn') as HTMLButtonElement | null;
if (this.btn) {
this.btn.addEventListener('click', () => this.export());
}
}

/** Capture the world and trigger a PNG download */
async export(): Promise<void> {
if (this.capturing || !this.btn) return;

this.capturing = true;
this.btn.disabled = true;

try {
// Hide day/night overlay so screenshot is clear
const overlay = this.world.dayNight.overlay;
const wasVisible = overlay.visible;
overlay.visible = false;

// Save current root transform and temporarily reset it
// so we capture the full world regardless of camera position
const root = this.world.root;
const savedX = root.x;
const savedY = root.y;
const savedScaleX = root.scale.x;
const savedScaleY = root.scale.y;

root.x = 0;
root.y = 0;
root.scale.set(1);

// Extract world.root to an offscreen canvas
const canvas = await (this.app.renderer as any).extract.canvas(root);

// Restore root transform
root.x = savedX;
root.y = savedY;
root.scale.set(savedScaleX, savedScaleY);

// Restore overlay visibility
overlay.visible = wasVisible;

// Convert canvas to PNG blob and download
await this.download(canvas);
} catch (err) {
console.error('Screenshot capture failed:', err);
} finally {
this.capturing = false;
if (this.btn) this.btn.disabled = false;
}
}

/** Convert canvas to blob and trigger download */
private download(canvas: HTMLCanvasElement): Promise<void> {
return new Promise<void>((resolve) => {
canvas.toBlob((blob) => {
if (!blob) {
resolve();
return;
}

const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `agent-move-screenshot-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.png`;
a.click();
URL.revokeObjectURL(url);
resolve();
}, 'image/png');
});
}

dispose(): void {
// Button is part of static HTML, no dynamic cleanup needed
this.btn = null;
}
}