TurboWarp extension

SVG Text

Responsive named styles and host-neutral plain/ruby text for TurboWarp.

日本語

Responsibility

SVG Text is a text provider. It defines text styles, computes host-neutral plain and ruby layout, creates and measures SVG text skins when a renderer is used, and releases the skins it owns. Bubble shape, tail, placement, portraits, and animation belong to the host package.

Kamishibai DSL 4.0 bundle

In the combined runtime palette, click the documentation button below the SVG Text member heading to return to this page. The two bundled blocks are the same style-definition and sprite-text blocks listed below; their member namespace and SVG Text icon identify their origin. Host-only measurement and lifecycle operations remain in the Composition API rather than the TurboWarp palette.

Installation

Install the Composition API with an exact version:

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

The standalone extension is also available from jsDelivr:

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

Host-neutral ruby layout

layoutRichText() accepts typed text and ruby runs. Ruby base and reading rows have explicit geometry and stay together as one wrapping and reveal unit. The layout also returns deterministic plainText and readingText projections.

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 includes padding. Text wraps at grapheme boundaries, but a ruby group is never split. An over-wide atomic ruby group grows the layout and sets overflow: true. Use plainText for visible/backlog text and choose explicitly whether accessibility or speech uses plainText or readingText. Renderer-backed setRichText() and measureRichText() use the same layout. No HTML, SVG markup, DOM node, URL, or event handler is accepted as content.

Blocks

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

Defines or replaces a named text style. Alignment is left, center, or right.

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

Creates an SVG skin and applies it to the current sprite drawable.

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 returns the maximum measured line width in stage-relative pixels. The composition owns every skin passed to it.

Host-neutral text layout

createSvgTextLayoutComposition() requires no runtime, renderer, skin, or drawable. Its synchronous layoutText() receives a named style, text, and an explicit renderer-native stage size. It returns deeply frozen JSON-compatible data: overall width and height, the required whitespace policy, normalized allowed style values, and each line's text, width, horizontal position, and 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: "First line\nSecond line",
  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);
}

Apply preserveWhitespace and assign text with textContent. The API accepts and returns no SVG markup, DOM nodes, event handlers, URLs, or foreignObject. The layout-only and renderer-backed paths share the same calculation for whitespace, font, size, alignment, line height, colors, placement, width, and height.

Standalone named-style handoff

From 0.8.1, the stock extension exposes a frozen getLayoutCapability(). Its layoutText() resolves the exact named-style registry populated by the define text style block and returns host-neutral data without creating a skin or drawable. Redefining a style changes subsequent layout results without exposing the mutable registry.

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

Use with TurboWarp Bubble

TurboWarp Bubble can consume this package through its Text Capability. Bubble core does not depend on SVG Text, and a different text provider can be injected.

Plain host-neutral layout first ships in 0.6.0, typed ruby layout in 0.8.0, and the stock named-style handoff in 0.8.1. Bubble #59 should pin 0.8.1 when its default overlay consumes styles defined through the standalone extension. TMPose Kamishibai #636 and other ruby consumers can use >=0.8.0 <0.9.0. Neither downstream package is imported here.