npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kubohiroya/turbowarp-svg-text

v0.9.0

Published

Host-neutral plain and ruby text layout with SVG text actors for TurboWarp.

Downloads

1,816

Readme

TurboWarp-SVG-Text

TurboWarp-SVG-Text provides responsive named styles, host-neutral plain/ruby layout, and SVG text actors. It is a text provider for host extensions such as turbowarp-bubble; Bubble shape, placement, portraits, and animation belong to the host.

Responsibilities

  • Define reusable text styles for font, size, color, background, and alignment.
  • Compute host-neutral plain and ruby line layout for DOM/SVG consumers without renderer APIs.
  • Create, replace, measure, and release SVG text skins when a renderer is used.
  • Keep text actors proportional to the stage.
  • Restyle visible text actors when a named style changes.

This package does not provide say or think bubbles, bubble tails, actor-relative placement, portrait images, or bubble animation. Those responsibilities belong to the host package and its capabilities.

Installation

Install the Composition API with an exact version:

pnpm add --save-exact @kubohiroya/[email protected]

The standalone TurboWarp extension is available from the version-pinned jsDelivr URL:

https://cdn.jsdelivr.net/npm/@kubohiroya/[email protected]/dist/svg-text.js

Blocks

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

Defines or replaces a named text style. Alignment accepts left, center, or right. The background is the rectangle behind an SVG text actor; a host Bubble can define a transparent background and render its own outer shape.

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

Creates an SVG skin and applies it to the current sprite's drawable. Newline characters render as separate lines. Reusing the same style name after redefinition redraws existing text actors.

Composition API

The composition API does not register the standalone extension. The host supplies a TurboWarp runtime and explicit targets.

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

const svgText = createSvgTextComposition({ runtime: Scratch.vm.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();

setText creates or replaces the SVG skin owned by the composition. measureText returns the maximum measured line width in stage-relative pixels. releaseTarget and releaseAll destroy skins owned by the composition.

Host-neutral text layout

createSvgTextLayoutComposition() needs no TurboWarp runtime, renderer, skin, or drawable. layoutText() is synchronous and accepts an explicit renderer-native stage size. It returns a deeply frozen, JSON-compatible data object containing overall width/height, the required whitespace policy, normalized allowed style values, and each line's text, measured width, horizontal x, and vertical baseline.

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

const svgText = createSvgTextLayoutComposition();
svgText.defineStyle({
  name: "dialogue-text",
  alignment: "center",
  backgroundColor: "transparent",
  font: "Noto Sans JP",
  fontPercent: 100,
  textColor: "#332200",
});

const layout = svgText.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));
textElement.setAttribute(
  "text-anchor",
  layout.style.alignment === "center"
    ? "middle"
    : layout.style.alignment === "right"
      ? "end"
      : "start",
);
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);
}

The host must apply preserveWhitespace and assign line content through textContent, as above. The API accepts and returns no SVG markup, DOM nodes, event handlers, URLs, or foreignObject; arbitrary input text remains plain line data. SvgTextComposition.layoutText() exposes the same contract on a renderer-backed composition. Both paths share the same layout calculation, so whitespace, font, computed font size, alignment, line height, colors, padding, corner radius, line placement, width, and height match setText().

Standalone named-style handoff

The stock TurboWarp extension exposes getLayoutCapability() from 0.8.1. The returned frozen capability resolves each request against the exact named-style registry populated by the define text style block, then returns the same host-neutral layoutText() data without creating a skin or drawable. Hosts can therefore preserve an existing project's font, color, size, alignment, and other named-style values when rendering text in a DOM/SVG overlay.

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],
});

The capability does not expose the mutable style map. Redefining a style through the stock extension changes subsequent layout results, while previously returned deeply frozen layout data remains unchanged. A missing style follows the stock extension's existing behavior and resolves through its default style.

Host-neutral ruby text layout

layoutRichText() accepts typed text and ruby runs. A ruby run keeps its base and reading together as one wrapping and reveal unit. The returned layout contains explicit geometry for both glyph rows, deterministic plainText and readingText projections, and no markup or executable values.

const svgText = createSvgTextLayoutComposition();
svgText.defineStyle({
  name: "dialogue-ruby",
  alignment: "left",
  backgroundColor: "transparent",
  font: "Noto Sans JP",
  fontPercent: 100,
  rubyFontPercent: 50,
  rubyGap: 1,
  textColor: "#332200",
});

const runs = [
  { type: "text", text: "海へ" },
  { type: "ruby", base: "出発", reading: "しゅっぱつ" },
  { type: "text", text: "!" },
] as const;
const layout = svgText.layoutRichText({
  styleName: "dialogue-ruby",
  runs,
  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: {
  text: string;
  x: number;
  baseline: number;
  fontSize: number;
}) => {
  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 is the requested outer width, including padding. Text wraps at grapheme boundaries and ruby groups are never split. If one atomic ruby group cannot fit, the layout grows to contain it and reports overflow: true; it never loops or silently shrinks the text. plainText keeps each ruby base, while readingText substitutes its reading. A host chooses which projection to use for accessibility or speech.

On a renderer-backed composition, setRichText() creates the SVG skin from the same layout and measureRichText() returns the maximum laid-out content width. Existing plain methods and Standalone blocks remain unchanged.

SvgTextComposition is intentionally suitable for a host Capability adapter:

interface TextCapability {
  setText(input: {
    styleName: string;
    target: { drawableID: number };
    text: string;
  }): void;
  measureText(input: { styleName: string; text: string }): number;
  releaseTarget(target: { drawableID: number }): void;
}

Integration with TurboWarp Bubble

@kubohiroya/turbowarp-bubble owns the Bubble outer shape, placement, portrait layers, and animation. It can use this package as its BubbleTextCapability, but the Bubble core does not depend on SVG Text itself.

The plain host-neutral contract is available from 0.6.0, the typed ruby contract from 0.8.0, and the stock named-style handoff from 0.8.1. TurboWarp Bubble #59 should pin 0.9.0 when its default overlay consumes styles defined through the standalone extension. TM Kamishibai #636, or another host that consumes layoutRichText(), can use the peer range >=0.9.0 <1. Neither downstream package is imported here. While this package is 0.x, breaking contract changes require a new minor version; patches may add or correct backward-compatible capabilities. To roll back the stock handoff, inject an independent layout composition or pin Bubble to a version that uses the explicit scratch-render backend.

Development

corepack enable
pnpm install --frozen-lockfile
pnpm run check

The version in package.json is the release source of truth. The consistency check keeps the exact-version README and Pages examples aligned and rejects a release tag that does not match it.

SPDX-License-Identifier: MPL-2.0