@event/visuals は「asset-kit に渡す React 要素」を組み立てるだけで、実際のラスタライズは @event/asset-kit の capture が担う。ビジュアルを画像/PDF にする経路は 2 系統あり、用途で使い分ける。
経路 A: DOM → ラスタ(asset-kit /capture)— スタジオの既定
ブラウザで組んだ DOM をそのまま画像化する。modern-screenshot(foreignObject = ブラウザ自身のレンダラ)を第一候補にし、失敗時のみ html2canvas-pro に fallback する。html2canvas は CSS を再実装するため字形・行送り・グラデが画面と乖離するが、foreignObject 方式は画面と一致する。
captureCanvas(element, fmt, bg, config) の手順が事故対策そのもの:
- 画面外に native 寸法(transform: none)で mount — scale されたプレビュー直撮りはサブピクセル丸めで文字がズレる
- 2 フレーム待ち →
document.fonts.ready - 外部画像を data URI に inline 化 — CORS 無し画像は
imgProxy経由で fetch、リトライ 3 回 + 併走上限 6。本番でだけロゴ/顔写真が抜ける真因は serverless コールドスタートへの同時 fetch 殺到で、この併走制御が根治策 - img の load 完了を待ってから
domToCanvasをexportScale倍で撮る
書き出し関数(全て client、ReactElement + ExportFmt を受ける):
await exportPng(el, fmt, bg, fileName); // canvas → PNG ダウンロード
await exportPdf(el, fmt, bg, fileName); // PNG(可逆) を pdf-lib で物理サイズ PDF に埋め込み
await exportAi(el, fmt, bg, fileName); // 素の PDF を .ai + application/illustrator で保存
await copyPngToClipboard(el, fmt, bg); // Figma へ Cmd+V(ClipboardItem に Promise<Blob>)
exportPdfが PNG(可逆)で埋めるのは、JPEG だと低 opacity の透かし階調が潰れて「プレビューと違う」事故になるため。fmt.pageMm(既定 A4)で A1 等の大判も 1 ページで入稿できるcaptureElementCanvas(source, fmt, bg)は 表示済み DOM を clone して撮る版。useLayoutEffectのテキストフィットが裏レンダだと間に合わず崩れるケース用
Format は ExportFmt 上位互換なので、visuals の Format を そのままこれらに渡せる:
await exportPng(<AnnounceCard input={input} fmt={fmt} brand={brand} />, fmt, brand.colors.bg, name);
適する場面: admin 主導・ピクセル完全一致・インタラクティブ(ブラウザに実フォントが載っていて、複雑な CSS を使える)。starter の /admin/visuals スタジオはこの経路。
経路 B: Satori / OG(server SVG → PNG)— per-request の共有カード
ブラウザが介在しない server 側で、React 要素を Satori が SVG 化 → PNG に焼く。next/og の ImageResponse がこれ。リクエストごとに生成してキャッシュする用途(4S user → 個人シェアカード = 第24章、OGP 画像)に向く。
ただし制約が経路 A と根本的に違う:
- Satori は CSS のサブセットしか解釈しない(flexbox 中心。grid・一部プロパティ非対応)。chrome/テンプレをそのまま流用できず、Satori 用に書き直す領域が要る
- フォントファイルの同梱が必須(system-ui は使えない。woff を bundle して渡す)
- 日本語の改行は自前で入れる必要があり、BudouX で文節分割(ZWSP 挿入)して渡すのが定石
これらの重い依存(bundled woff loader / budoux)は visuals では現状 deferred。index.ts にも「Satori/OG 経路で必要になった時点で追加する」と明記してある。参照実装は ivs-cms の src/lib/survey/fonts.ts(fontsourceWoff)と src/lib/jpSegment.ts(segmentJa)。
どちらを選ぶか
| 判断軸 | DOM→PNG/PDF(A) | Satori/OG(B) |
|---|---|---|
| 実行場所 | client(ブラウザ) | server(edge/node) |
| きっかけ | admin の書き出しボタン | HTTP リクエスト(OGP 等) |
| 忠実度 | 画面と完全一致 | CSS サブセット近似 |
| フォント | ブラウザ実フォント | woff 同梱必須 |
| 印刷 PDF/.ai | 対応(pageMm) | 非対応(PNG のみ) |
| 向く用途 | 会場パネル・SNS 告知の手動書き出し | 個人シェアカードの自動大量生成 |
原則: 人が 1 枚ずつ仕上げて入稿する印刷/SNS は経路 A、リクエストごとに自動生成してキャッシュする OG は経路 B。