ガイド 第18

セッションカード レイアウトエンジン

@event/session-card

@event/session-cardengine/ は React 非依存の純関数群である。3リポに4コピーあった balance engine(最多重複コード)をここに一本化した。移植は ivs-cms cardLayouts.tsx数式・分岐を同一に保つ関数化であり、JSDoc にも「数式・分岐は同一」と明記している — つまり ivs の実カードとの挙動差分ゼロが移植の受け入れ条件である(resolveTitleLines / resolveRoleLines は描画側と同一式を共有する切り出しそのもの)。

設計原則(1920×1080 master frame)

  1. 写真をぎちぎちまで大きく取る(photo-first balance)
  2. タイトルは1行が基本、相当長い場合のみ2行
  3. 名前は省略しない(overlap "name plate" でフレーム外まで許容)
  4. 役職は短ければ大きく1〜2行、長ければフォントを下げて最大3〜4行で全文表示(予約高は据え置きなので写真サイズは不変)

variant は "1A".."10A"(数字 = 並べる登壇者数)。保存された cardVariant が人数より少ないと写真が黙って欠けるため、effectiveVariant(cardVariant, speakerCount) が最低でも人数まで枠を自動補正する(明示的に多い枠は尊重して下げない)。

visualWidth — 幅の通貨

全ての fit 計算は px 実測ではなく概算幅 visualWidth(CJK = 1.0 / ASCII = 0.55 em)で行う。これにより server / client / satori のどこでも同じ答えが出る(決定論)。CHAR_WIDTH_MULT = 1.0 は「CJK は palt + letter-spacing 詰めでも1文字 ≒ 1em、ASCII は visualWidth 側で 0.55 に圧縮済み」という前提の係数である。

computeBalance — {titleSize, titleLines, photoSize, gap} を解く

上タイトル / 下スピーカー構成(1A–4A 系)の配分ソルバ。可用高 available = H - TICKER_H - METAROW_H - FOOTER_AREA - topPad の中で、写真を大きい方から 10px 刻みで下げながら探索する:

  • Phase A: 1行 + 基準フォントのまま入る最大写真
  • Phase B: 1行のままフォントを縮小(下限 minTitleSize
  • Phase C+: L = 2..maxTitleLines。長文は「2行に小さく詰め込む / 切る」のではなく行数を増やして読めるフォントを保つ。中間の L では下限フォントを死守し、届かない長文は次の行数へ回す。最終 L だけ幅頭打ちを許容して必ず収める
  • Fallback: maxTitleLines 行 × 最小フォント(切らない

英語(Latin)タイトルは字面が大きく見えるため基準フォントを 0.8 倍に抑える補正が入る。computeGridBalance は同じ Phase 探索を rows × cols グリッド(5A–10A)に拡張し、カラム幅 colW = (contentWidth - (cols-1)*colGap) / cols を写真サイズの上限 cap にする。6A–10A は DENSE_DEFAULTS(maxTitleLines: 2、minPhotoSize: 240 等)を使う。

breakTitleLines — 決定論的改行

text-wrap: balance + word-break: auto-phrase 任せでは CJK が文節途中で割れて不安定だった。ここでは改行候補を品質ペナルティ付きで列挙し、DP で「行長の均し(raggedness)+ 境界ペナルティ」を最小化する:

境界penalty
スペース区切り0
句読点直後(、。・〜 等)2
閉じ括弧直後 / 開き括弧直前3
文節(任意の CJK 境界、最後の手段)9

禁則も守る: 行頭に閉じ括弧・句読点・小書き仮名・長音を置かない、行末を開き括弧で終えない、英単語の途中で割らない。PENALTY_CLEAN = 3(良い境界のみ)と PENALTY_PHRASE = 9(文節も許可)の2段があり、fitTitleLayout は「clean で割れる最大フォント → phrase 許可 → 面積 fit」の順に落ちる。

役職と名前の fit

  • fitRole — 予約高(fsRoleMax × 1.25 × 3 行)内で、clean 境界 → phrase 境界の2パスで割れる最大フォントを探す。「代表取締役」を「代表取 / 締役」と語中で割らない
  • uniformRoleSize — カード内全登壇者の fitRole 結果の最小値を採り、役職フォントを1サイズに統一する(一番長い役職に全員を合わせる)
  • uniformNameSize英語(Latin)名のみを1サイズに統一。CJK 名は個別 fit のまま
  • fitSpeakerName — 1行優先(floor = 基準の 0.42 倍まで縮小して1行維持)、それでも入らない長名のみ中黒 / スペース境界で2行に折る。nameScales[id] の per-speaker 手動倍率が最後に掛かる
  • fitSpeakerRole — 手動上書き(\n 明示改行)> 統一サイズでの自動改行 > 個別 auto-fit の優先順。上書きはページ言語と一致するときだけ採用する(overrideForLang

これらは第19章のデザイナーが onResolved 経由でそのまま参照する。engine の数値(W=1920 / TICKER_H=80 / TITLE_LINE_RESERVE=1.04 等)は engine/constants.ts に集約されており、変更はレイアウト全体に波及するため原則触らない。