Phase 8: session tooling — handouts to players

- GM "📣 Handout" composer in the header (title + body + optional image) sets
  uiStore.activeHandout; a clear button hides it.
- snapshot gains an optional handout {title, body, imageId}; buildSnapshot streams
  the handout image over the existing image channel (id "handout").
- Player view (local + networked) shows the handout as a prominent card above the
  boards. resizeToMax() keeps handout images compact.
- (Live scene/map switching already works via "Show to players".)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 15:48:23 +02:00
parent f923375ebb
commit 7f1ab8f094
10 changed files with 125 additions and 6 deletions
+20
View File
@@ -16,6 +16,26 @@ function loadImage(src: string): Promise<HTMLImageElement> {
});
}
/** Downscale an image to fit within maxDim (preserving aspect), as a JPEG data URL. */
export async function resizeToMax(src: string, maxDim = 1024, quality = 0.85): Promise<string> {
try {
const img = await loadImage(src);
const scale = Math.min(1, maxDim / Math.max(img.naturalWidth, img.naturalHeight));
const w = Math.max(1, Math.round(img.naturalWidth * scale));
const h = Math.max(1, Math.round(img.naturalHeight * scale));
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return src;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, w, h);
return canvas.toDataURL('image/jpeg', quality);
} catch {
return src;
}
}
/** Natural pixel size of an image data URL. */
export function imageSize(src: string): Promise<{ w: number; h: number }> {
return loadImage(src).then((img) => ({ w: img.naturalWidth, h: img.naturalHeight }));