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

@overpunch/wraptype

v1.1.1

Published

Live DOM text wrapping any 3D surface — built-in shapes, custom meshes, orbit camera, and an optional WebGL/SDF renderer

Readme

wrapType

npm npm downloads License: MIT part of liiift type-tools

Real DOM text on any 3D surface — sphere, cylinder, torus, plane, waving flag, stool, or a custom mesh.

wrapType uses Three.js's CSS3DRenderer to distribute HTML text elements across the geometry of a 3D surface. Each character is a real DOM element oriented along the surface normal, which means variable fonts, CSS animations, hover states, and every other Liiift tool compose naturally — no canvas, no textures.

Try it live and drop in your own .glb / .gltf / .obj mesh at wraptype.com.

Install

npm install @overpunch/wraptype three

Only three is required. The remaining peers are optional — install the ones you use:

| Peer dependency | Needed for | |---|---| | three (required) | Core geometry + CSS3DRenderer | | react, react-dom | The WrapTypeScene component / useWrapType hook | | @react-three/fiber, troika-three-text | The GPU/SDF renderer at @overpunch/wraptype/r3f |

# React + DOM renderer (most common):
npm install @overpunch/wraptype three react react-dom

# R3F / WebGL renderer:
npm install @overpunch/wraptype three @react-three/fiber troika-three-text

Vanilla JS users need only three: import from @overpunch/wraptype/core (the main entry also exports the React components, so it imports react). The React-free SDF factory is at @overpunch/wraptype/sdf (three + troika-three-text).

React

Component

'use client' // Next.js App Router: WrapTypeScene renders in the browser only.

import { WrapTypeScene } from '@overpunch/wraptype'

<WrapTypeScene
  text="Typography is the art and technique of arranging type"
  shape="sphere"
  fill="cover"
  fontSize={13}
  color="rgba(220,210,255,0.85)"
  autoRotate
  style={{ width: '100%', height: '500px' }}
/>

Hook

'use client'

import { useWrapType } from '@overpunch/wraptype'

export function Ring() {
  const { ref } = useWrapType({
    text: 'Typography is the art and technique of arranging type',
    shape: 'cylinder',
    fill: 'flow',
  })
  // The container needs a size: the scene fills it.
  return <div ref={ref} style={{ width: '100%', height: '500px' }} />
}

Next.js App Router

The React components are browser-only, and the published bundle does not carry a 'use client' directive, so import them from a file marked 'use client' (as above) and render that from your Server Component page:

// app/page.tsx (a Server Component)
import { Ring } from './Ring' // the 'use client' file above

export default function Page() {
  return <Ring />
}

Vanilla JS

import { getCharPositions, createWrapScene } from '@overpunch/wraptype/core'

// The container needs a size (e.g. width: 100%; height: 500px): the scene fills it.
const container = document.getElementById('scene')

// Pass null as the positions: the scene measures each character's width (proportional spacing) and lays
// the text out itself.
const scene = createWrapScene(container, null, {
  text: 'Typography is the art and technique of arranging type',
  shape: 'torus',
  fill: 'cover',
  autoRotate: true,
  autoRotateSpeed: 0.6,
})

// Later, to clean up:
scene.destroy()

// Your own positions (e.g. from getCharPositions with a charWidthMap, or getCharPositionsFromMesh):
const newPositions = getCharPositions({ text: 'New text', shape: 'sphere' })
scene.rebuild(newPositions)

GPU / WebGL renderer (/r3f)

The default renderer places real HTML in 3D (CSS3DRenderer). For text that needs to live inside a WebGL scene — receiving lights, shaders, and post-processing — import the SDF renderer from the @overpunch/wraptype/r3f entry point. It uses troika-three-text for GPU-antialiased glyphs curved onto the surface via troika's native curveRadius.

Requires @react-three/fiber and troika-three-text as peers.

React Three Fiber component

import { Canvas } from '@react-three/fiber'
import { WrapTypeMesh } from '@overpunch/wraptype/r3f'

<Canvas>
  <WrapTypeMesh
    shape="sphere"
    radius={1}
    text="Typography on any surface"
    color="#ffffff"
    fontSize={0.1}
    autoRotate
  />
</Canvas>

WrapTypeMesh accepts shape ('sphere' | 'cylinder' | 'torus' | 'plane'; troika curves text around a vertical axis only, so sphere and torus render as a band of words around the equator, and flag and stool fall back to plane), radius, text, font, fontSize, letterSpacing, maxWidth, textAlign, color, curvatureTracking, curvatureTrackingFactor, plus standard transform props (position, rotation, scale, autoRotate, autoRotateSpeed).

Imperative (no React)

import { createSDFText, updateSDFText } from '@overpunch/wraptype/sdf'

const { group, dispose } = createSDFText('cylinder', {
  text: 'Typography on any surface',
  fontSize: 0.1,
  color: '#ffffff',
}, /* radius */ 1)

scene.add(group)

// Rebuild in place — keeps the same group reference:
updateSDFText(group, 'torus', { text: 'New text', color: '#ffccaa' }, 1)

// Free GPU resources when done:
dispose()

| When to use | Renderer | Import | |---|---|---| | Variable fonts, CSS animation, selectable text, compose with other Liiift tools | DOM (CSS3DRenderer) | @overpunch/wraptype | | Text inside a WebGL scene — lighting, shaders, post-processing, large glyph counts | SDF (WebGL) | @overpunch/wraptype/r3f |

API

WrapTypeScene props / WrapTypeOptions

| Option | Type | Default | Description | |---|---|---|---| | text | string | — | Text to distribute across the surface. Repeats to fill. | | shape | 'sphere' \| 'cylinder' \| 'torus' \| 'plane' \| 'stool' \| 'flag' | 'sphere' | Built-in 3D geometry to wrap text onto. | | mode | 'surface' | 'surface' | Places characters on the geometry. ('silhouette' is reserved and not implemented yet; it falls back to 'surface' with a warning.) | | fill | 'cover' \| 'flow' \| 'full-width' \| 'full-height' | 'cover' | How to distribute characters across the surface. ('pattern' is reserved; it falls back to 'cover'.) | | fontSize | number | 14 | Character font size in px. | | fontFamily | string | undefined | CSS font-family override applied to each character span. | | fontWeight | string \| number | 'normal' | CSS font-weight applied to each character span. | | color | string | 'white' | CSS color applied to every character element. | | radius | number | 300 | Surface radius in scene units. | | height | number | radius * 2 | Cylinder or plane height in scene units. | | autoRotate | boolean | false | Continuously rotate the scene. | | autoRotateSpeed | number | 1.0 | Rotation speed multiplier. | | camera | 'orbit' \| 'fixed' | 'orbit' | 'orbit' — drag to rotate, scroll to zoom. 'fixed' — static camera. | | zoom | boolean | true | Wheel / pinch zoom on the orbit camera. Set false so the mouse wheel scrolls the page over the scene. | | showBackfaces | boolean | false | Show characters on the far side, seen through the surface (mirrored). Hidden by default. | | cameraPosition | [number, number, number] | [0, 0, 700] | Initial camera position in scene units. | | charAdvanceRatio | number | 0.62 | Fraction of fontSize used as a character's advance when it has no measured width (charWidthMap; the scene and the React components measure widths for you). | | charWidthMap | Map<string, number> | measured | Advance width per character, in px (see measureCharWidths). | | lineHeightRatio | number | 1.4 | Line height multiplier relative to fontSize. | | repeat | boolean | true | When false, text is placed exactly once without tiling; text that doesn't fit is left out with a warning. | | characterCurve | number | 0 | Bend characters to follow surface curvature. 0 = flat, 1 = full bend. | | positions | CharPosition[] | — | Pre-computed positions (e.g. from getCharPositionsFromMesh); when given, they replace the built-in shape layout. (React only; in vanilla JS pass them to createWrapScene.) | | style | CSSProperties | — | Applied to the container div. (React only) |

Fill modes

  • cover — tiles the full surface in rows (latitude bands on the sphere); the text continues from row to row
  • flow — a single band around the equator / circumference; the text repeats to fill it (with repeat: false, text longer than the band is cut)
  • full-width — (sphere) the text once around the equator, scaled to fit it exactly
  • full-height — (sphere) one character per line down the meridian facing the camera
  • Rows and rings are justified, so the text meets itself without a seam or overlap. On other shapes, full-width and full-height use cover.

Every character faces outward and reads left to right from outside the surface. Text is laid out by grapheme, so emoji sequences and accented letters stay whole; right-to-left scripts are laid out in logical order (left to right).

SceneHandle

Returned by createWrapScene():

| Method | Description | |---|---| | destroy() | Removes the renderer and all event listeners. | | rebuild(positions) | Replaces all character elements without rebuilding the renderer (custom positions also stop the flag's own animation). |

The render loop runs only while something moves (dragging, damping, auto-rotation, the flag's wave) and while the scene is on screen. prefers-reduced-motion stops auto-rotation and the flag's wave, including when the setting changes while the scene runs. Sizes are validated: zero, negative and non-numeric fontSize, radius and ratios fall back to their defaults, and a layout is capped at 20,000 characters (with a warning).

Accessibility

The 3D characters repeat the text, one letter per element, so they are hidden from screen readers (aria-hidden), and the text is exposed once in a visually hidden element inside the container: screen readers read it, and find-in-page finds it. Give WrapTypeScene an aria-label or role="img" if the scene needs a description; HTML attributes are forwarded to the container. The glyphs themselves are not selectable.

getCharPositions(opts)

Returns CharPosition[] — one entry per character instance placed on the surface.

interface CharPosition {
  char: string
  position: [number, number, number]  // world position
  normal:   [number, number, number]  // outward surface normal
  right:    [number, number, number]  // tangent along text direction
  up:       [number, number, number]  // tangent perpendicular to right
}

getCharPositionsAt(opts, t)

Like getCharPositions, but evaluates positions at a specific time parameter t — useful for animating the geometry.

isAnimatedShape(shape)

Returns true if the given shape name requires time-stepped updates (e.g. 'flag').

How it works

Three.js's CSS3DRenderer maps HTML elements into 3D space using CSS perspective and matrix3d transforms. wrapType computes character positions analytically from the shape's surface equations — no UV unwrapping or texture atlases.

Each character element is oriented so its visible face points along the outward surface normal using a rotation matrix built from the surface's local right/up/normal frame.

OrbitControls are wired to a transparent overlay <div> so that pointer events reach the controls without selecting text.

Shapes

| Shape | Notes | |---|---| | Sphere | Latitude bands from φ = 0.12π to 0.88π. Band character count scales with sin(φ). | | Cylinder | Characters arc around the circumference, stacked in rows. | | Torus | flow: characters follow the outer ring. cover: rings of text over the whole tube, parameterised by major and minor angle; seen from the front, the inside of the tube reads upside down. | | Plane | Grid of characters on a flat surface facing the camera. | | Stool | Cylinder with disc caps — top, side, and bottom surfaces sampled separately. | | Flag | Animated surface — characters follow a sinusoidal wave that propagates along the flag. |

Custom meshes

Drop any .glb, .gltf, or .obj file onto the demo. wrapType samples the surface with MeshSurfaceSampler, orients each character along the local normal, and auto-scales to fit the scene radius.

In code, load any Three.js Mesh and feed it to getCharPositionsFromMesh, then render the result through the same scene API (or pass positions to <WrapTypeScene> in React):

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import type { Mesh } from 'three'
import { getCharPositionsFromMesh, createWrapScene } from '@overpunch/wraptype/core'

new GLTFLoader().load('/model.glb', (gltf) => {
  // The first mesh in the file (merge multi-part models into one mesh before exporting).
  const mesh = gltf.scene.getObjectByProperty('isMesh', true) as Mesh | undefined
  if (!mesh) return

  const positions = getCharPositionsFromMesh(
    mesh,
    'Typography on any surface',
    { radius: 300 },
    250, // sample count
  )
  // text is required by the options type; with custom positions, the scene uses it for screen readers.
  createWrapScene(document.getElementById('scene')!, positions, { text: 'Typography on any surface', autoRotate: true })
})

Requirements

| | | |---|---| | Runtime | Modern evergreen browsers (CSS3DRenderer needs transform-style: preserve-3d + matrix3d). | | Peer deps | three >= 0.160. Optional: react/react-dom >= 17, @react-three/fiber >= 8, troika-three-text >= 0.47. | | Module formats | ESM + CJS, with bundled TypeScript declarations. | | SSR | Browser-only. In Next.js App Router, mark the consuming component 'use client'. |

Performance notes

The DOM renderer creates one HTML element per character instance, so cost scales with the number of placed glyphs (which fill, repeat, and surface area all affect). It is built for striking display typography — wrap a headline, a logo, a hero — not for paragraphs of thousands of characters. In headless Chrome (software rendering), an auto-rotating sphere of about 2,000 characters ran at a ~45 ms median frame and the default sphere (about 6,000) at ~150 ms; real GPUs are much faster. A static scene does no work between interactions. For very high glyph counts, or to embed text inside a lit/shaded WebGL scene, use the SDF renderer instead.

How it compares

  • drei <Text3D> / extruded geometry — renders solid 3D letterforms in WebGL. wrapType's DOM mode instead keeps glyphs as flat, real HTML on the surface, styleable with CSS, with the text exposed once to screen readers.
  • Plain troika-three-text — gives you GPU SDF text but not surface distribution. wrapType's /r3f renderer wraps troika with shape geometry and curvature; its DOM renderer needs no WebGL text at all.
  • CSS-only 3D transforms — can fake perspective on a block of text, but cannot distribute individual characters analytically across a curved surface with correct per-glyph normals. That is wrapType's core.

Contributing

git clone https://github.com/over-punch/wrapType.git
cd wrapType
npm install

npm test        # vitest unit tests (happy-dom)
npm run typecheck
npm run build   # vite library build → dist/ (ESM + CJS + types)

The package source lives in src/ (core/ is framework-agnostic; react/ holds the hook + component; r3f/ is the SDF renderer). The landing page and interactive demo are a separate Next.js app in site/. README visuals are regenerated reproducibly with npm run capture: it drives a local build of the demo (cd site && npx next build && npx next start -p 5961) with Playwright and needs ffmpeg for the GIF; see scripts/capture.mjs.

License

MIT © Overpunch (see LICENSE)

Part of type-tools — a suite of typographic tools for techniques that are impossible or impractical in CSS alone.