TurboWarp拡張

SVG Text

名前付きstyleによるhost-neutralなplain/ruby文字とSVGテキストアクター。

English

責務

SVG Textは文字providerです。文字styleの定義、host-neutralなplain/ruby layout計算、renderer使用時のSVG文字skinの生成・測定・更新・解放を担当します。 吹き出しの外枠、tail、配置、ポートレイト、アニメーションはhost側の責務です。

紙芝居DSL 4.0集約ランタイム

集約ランタイムのパレットでSVG Text見出し直下の ドキュメントボタンを押すと、このページを開けます。集約される2つのブロックは、 下記の文字style定義とsprite文字設定であり、拡張機能別の名前空間とSVG Textの アイコンで由来を識別できます。host専用の測定・ライフサイクル操作は TurboWarpパレットではなくComposition APIにあります。

インストール

Composition APIはversionを固定してインストールします。

pnpm add --save-exact @kubohiroya/turbowarp-svg-text@0.8.1

単体のTurboWarp拡張はjsDelivrからも読み込めます。

https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-svg-text@0.8.1/dist/svg-text.js

Host-neutral ruby layout

layoutRichText()は型付きのtextruby runを受け取ります。rubyのbaseとreadingは個別のgeometryを持ち、折返しとrevealでは1つの原子的な単位として扱います。layoutは確定的なplainTextreadingText射影も返します。

const textLayout = createSvgTextLayoutComposition();
textLayout.defineStyle({
  name: "dialogue-ruby",
  backgroundColor: "transparent",
  font: "Noto Sans JP",
  rubyFontPercent: 50,
  rubyGap: 1,
});
const layout = textLayout.layoutRichText({
  styleName: "dialogue-ruby",
  runs: [
    { type: "text", text: "海へ" },
    { type: "ruby", base: "出発", reading: "しゅっぱつ" },
  ],
  nativeSize: [480, 360],
  maxWidth: 240,
});

const svgNamespace = "http://www.w3.org/2000/svg";
const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
const textElement = document.createElementNS(svgNamespace, "text");
textElement.setAttributeNS(xmlNamespace, "xml:space", "preserve");
textElement.setAttribute("fill", layout.style.textColor);
textElement.setAttribute("font-family", layout.style.font);
const appendGlyph = (glyph) => {
  const tspan = document.createElementNS(svgNamespace, "tspan");
  tspan.setAttribute("x", String(glyph.x));
  tspan.setAttribute("y", String(glyph.baseline));
  tspan.setAttribute("font-size", String(glyph.fontSize));
  tspan.textContent = glyph.text;
  textElement.append(tspan);
};
for (const line of layout.lines) {
  for (const fragment of line.fragments) {
    if (fragment.type === "text") {
      appendGlyph({ ...fragment, fontSize: layout.style.fontSize });
    } else {
      appendGlyph(fragment.reading);
      appendGlyph(fragment.base);
    }
  }
}

maxWidthはpaddingを含みます。textはgrapheme境界で折り返しますが、ruby groupは分割しません。原子的なrubyが幅を超える場合はlayoutを拡張してoverflow: trueを返します。表示/backlogにはplainTextを使い、accessibilityや読上げでplainTextreadingTextのどちらを使うかはhostが明示的に選択します。renderer-backedのsetRichText()measureRichText()も同じlayoutを使います。contentとしてHTML、SVG markup、DOM node、URL、event handlerを受理しません。

ブロック

define text style [STYLE] background [BACKGROUND] text [TEXT_COLOR] font [FONT] size [SIZE] align [ALIGN]

名前付き文字styleを定義または置換します。alignmentはleft、center、rightから選びます。

set this sprite text [TEXT] with style [STYLE]

現在のsprite drawableへSVG文字skinを適用します。

Composition API

import { createSvgTextComposition } from "@kubohiroya/turbowarp-svg-text/composition";

const svgText = createSvgTextComposition({ runtime });
svgText.defineStyle({
  name: "dialogue-text",
  alignment: "left",
  backgroundColor: "transparent",
  font: "Noto Sans JP",
  fontPercent: 100,
  textColor: "#332200",
});
svgText.setText({ styleName: "dialogue-text", target, text: "The End" });
const width = svgText.measureText({ styleName: "dialogue-text", text: "The End" });
svgText.releaseTarget(target);
svgText.releaseAll();

measureTextはstage相対pixelで最大行幅を返します。compositionは渡されたskinを所有します。

Host-neutral文字layout

createSvgTextLayoutComposition()はruntime、renderer、skin、drawableを必要としません。 同期APIのlayoutText()へ名前付きstyle、文字列、明示的なrenderer native stage sizeを渡すと、deep freezeされたJSON互換dataを返します。dataには全体の幅・高さ、 必須の空白保持policy、正規化済みの許可style値、各行の文字列・幅・水平位置・baselineが含まれます。

import { createSvgTextLayoutComposition } from "@kubohiroya/turbowarp-svg-text/composition";

const textLayout = createSvgTextLayoutComposition();
textLayout.defineStyle({
  name: "dialogue-text",
  alignment: "center",
  backgroundColor: "transparent",
  font: "Noto Sans JP",
  fontPercent: 100,
  textColor: "#332200",
});
const layout = textLayout.layoutText({
  styleName: "dialogue-text",
  text: "1行目\n2行目",
  nativeSize: [480, 360],
});

const svgNamespace = "http://www.w3.org/2000/svg";
const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
const textElement = document.createElementNS(svgNamespace, "text");
if (layout.preserveWhitespace) {
  textElement.setAttributeNS(xmlNamespace, "xml:space", "preserve");
}
textElement.setAttribute("fill", layout.style.textColor);
textElement.setAttribute("font-family", layout.style.font);
textElement.setAttribute("font-size", String(layout.style.fontSize));
for (const line of layout.lines) {
  const tspan = document.createElementNS(svgNamespace, "tspan");
  tspan.setAttribute("x", String(line.x));
  tspan.setAttribute("y", String(line.baseline));
  tspan.textContent = line.text;
  textElement.append(tspan);
}

preserveWhitespaceを適用し、文字はtextContentへ代入してください。このAPIはSVG markup、DOM node、event handler、URL、foreignObjectを入力・返却しません。layout-only経路とrenderer経路は、 空白、font、size、alignment、line height、色、配置、幅、高さの計算を共有します。

Standalone named-style handoff

0.8.1からstock拡張はfrozenなgetLayoutCapability()を公開します。layoutText()define text styleブロックが更新する同じnamed-style registryを解決し、skin/drawableを生成せずhost-neutralなdataを返します。mutableなregistryは公開せず、style再定義は次回以降のlayout結果へ反映されます。

const svgTextExtension = Scratch.vm.runtime.ext_kubohiroyasvgtext;
const textLayouts = svgTextExtension.getLayoutCapability();
const layout = textLayouts.layoutText({
  styleName: "dialogue-text",
  text: "既存named style",
  nativeSize: [480, 360],
});

TurboWarp Bubbleとの併用

TurboWarp Bubbleは Text Capability経由でこのpackageを利用できます。Bubble coreはSVG Textへ依存せず、別の文字providerも注入できます。

Plain host-neutral layoutは0.6.0、型付きruby layoutは0.8.0、stock named-style handoffは0.8.1で初めて提供します。 Bubble #59で既定overlayからstandalone拡張のstyleを使う場合は0.8.1へ固定します。 TMPose Kamishibai #636などのruby consumerは>=0.8.0 <0.9.0を使えます。どちらの下流packageもこのpackageからimportしません。