カードのタイトル / 役職 / 氏名の改行は、ブラウザの text-wrap: balance + word-break: auto-phrase 任せをやめて、@event/session-card の engine/text.ts が決定論的に解く。CJK は辞書ヒューリスティックで文節途中に割れることがあり(例「巨額の資金調」/「達、…」)、改行位置が実行のたびにブレるためである。ここで実装されているのは BudouX ではなく、良い境界 + 行長均しの DP だ。BudouX / bundled-woff は @event/visuals に未実装の計画(deferred)として JSDoc に明記されている(後述)。
visualWidth — 幅の通貨(CJK=1.0 / ASCII=0.55 em)
px 実測を使わず、visualWidth(text) が CJK を 1.0 em、ASCII を 0.55 em として概算する。server / client / satori のどこでも同じ答えが出る(決定論)。maxPerLine = widthBudget / (fontSize * mult) が「その fontSize で 1 行に入る visualWidth 上限」になる。
candidateCuts — 改行境界のペナルティ表
candidateCuts(text) が改行可能位置を品質ペナルティ付きで列挙する(低いほど良い):
| 境界 | penalty | 判定に使う定数 |
|---|---|---|
| スペース区切り | 0 | isSpace(prev) |
| 句読点直後 | 2 | PUNCT_AFTER = 、。,.・…〜~::// |
| 閉じ括弧直後 | 3 | CLOSE_AFTER = ))」』〉》〕}]> |
| 開き括弧直前 | 3 | OPEN_BRACKETS |
| 文節境界(辞書なし・最後の手段) | 9 | 上記いずれでもない CJK 境界 |
禁則も同時に守る: 行頭に閉じ括弧 / 句読点 / 中黒 / 長音 / 小書き仮名を置かない(LINE_START_FORBIDDEN)、開き括弧で行を終えない(OPEN_BRACKETS が prev)、英単語(ASCII)の途中で割らない(isAsciiWord 連続)。許可する境界品質の上限は 2 段: PENALTY_CLEAN = 3(スペース / 句読点 / 括弧まで)と PENALTY_PHRASE = 9(文節も許可)。
breakTitleLines — 行長均し DP
breakTitleLines(text, maxPerLine, maxLines, maxPenalty) が本体。1 行に収まれば [text]、maxPenalty 以下の境界で割り切れなければ null(呼び出し側でフォント縮小へ)。DP は各分割の raggedness((maxPerLine - w) 2)+ 境界ペナルティ × PEN_W(36)** を最小化する。最終行は短くても罰しない。
fitTitleLayout — フォント × 改行の確定
import { fitTitleLayout, visualWidth } from "@event/session-card";
const { fontSize, lines } = fitTitleLayout(title, widthBudget, baseFontSize, 3);
// 優先順:
// 1. baseFontSize で 1 行に入るならそのまま
// 2. clean 境界(<=3)で maxLines 行に割れる最大フォント(1→minScale=0.5 を step 0.04 で下降)
// 3. clean 不可 → phrase 境界(9)も許可して同じ探索
// 4. 極端な長文 → 面積 fit(floor(maxLines * widthBudget * slack / (visualWidth * mult)))で必ず収める
「2 行に小さく詰める / 切る」のではなく行数を増やして読めるフォントを保つのが方針。slack=1.08 は letter-spacing -0.05em + palt 文字詰めで実幅が visualWidth の約 0.9 倍になるぶんの余裕。
役職 / 氏名の fit
fitRole— clean → phrase の 2 パス。予約高fsRoleMax × 1.25 × 3 行を超えない最大フォントを採る。goodBreakSegments(clean cut で割った最長セグメント)が縮小の下限を決め、「代表取締役」を「代表取 / 締役」と語中で割らないfitSpeakerName— 1 行優先(floor = 基準 0.42 倍まで縮小して 1 行維持)。それでも入らない長名だけsplitNameForWrap(中黒・スペースの中央寄り境界)で 2 行に折る。英語名はforcedNameSizeで 1 サイズに統一、CJK 名は個別 fithasCJK/CJK_RE(/[-ヿ㐀-鿿ヲ-゚]/)で言語を判定し、overrideForLangは手動上書き(cardRoles等)をページ言語と一致するときだけ採用する。pickLangは en 空欄で ja へフォールバック
タイトル前処理として stripPoweredBy(協賛クレジットを落として改行崩落を防ぐ)と parseManualLines(\n 明示改行を行配列に解す)も同居する。
BudouX / bundled-woff は未実装(deferred)
章題の「BudouX」「punct fallback woff」は現状コードには無い。@event/visuals/index.ts の JSDoc に「フォントファイル同梱(bundled-woff-first loader)と BudouX 文節分割(ZWSP 挿入)は Satori/OG 経路で必要になった時点で追加する。重い依存(budoux 等)は今は入れない」と明記されている。参照実装として ivs-cms の survey/fonts.ts(fontsourceWoff)/ jpSegment.ts(segmentJa)が挙がるが、移植は保留。つまり DOM カード = titleBreak DP(実装済)/ Satori・OG の BudouX + 同梱 woff = 設計のみ、という線引きで読むこと。