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

@sorrell/ink-three

v1.0.0

Published

Render ThreeJS scenes in the terminal with ink.

Readme

@sorrell/ink-three

Render Three.js scenes in the terminal with Ink.

@sorrell/ink-three uses Three.js for scene graphs, geometry, transforms, and camera projection, then rasterizes the result into terminal cells. It can render wireframes, flat-shaded faces, material and object colors, and several Unicode glyph formats without WebGL, Canvas, DOM APIs, or native graphics dependencies.

The package includes three ways to render:

  • InkThreeView.InkThreeView, a React component for Ink applications.
  • Terminal.Renderer, a synchronous API for producing text or structured frames.
  • SceneRenderer and RendererRuntime, Effect services for validation, dependency injection, frame sinks, terminal capabilities, and interruptible render loops.

Installation

Using NodeJS 24.18.0 or later, run

npm install @sorrell/ink-three effect ink react three

ink, react, and three are peer dependencies; these are all required.

Ink example

The following is a complete Ink application. It creates a small Three.js scene, updates its transforms, and lets InkThreeView redraw it as terminal cells.

import * as Ink from "ink";
import * as React from "react";
import * as THREE from "three";
import {
    InkThreeView,
    Terminal
} from "@sorrell/ink-three";

interface ExampleScene
{
    readonly Camera: THREE.PerspectiveCamera;
    readonly Group: THREE.Group;
    readonly Scene: THREE.Scene;
}

function App(): React.ReactElement
{
    const { exit } = Ink.useApp();
    const { columns, rows } = Ink.useWindowSize();
    const Width = Math.max(1, columns);
    const Height = Math.max(1, rows - 1);

    const Example = React.useMemo<ExampleScene>(() =>
    {
        const Scene = new THREE.Scene();
        const Camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
        const Group = new THREE.Group();
        const Cube = new THREE.Mesh(
            new THREE.BoxGeometry(1.8, 1.8, 1.8),
            new THREE.MeshBasicMaterial({ color: "#55aaff" })
        );
        const Orbit = new THREE.Line(
            new THREE.BufferGeometry().setFromPoints(
                new THREE.EllipseCurve(0, 0, 2.2, 1.1)
                    .getPoints(64)
                    .map(({ x, y }) => new THREE.Vector3(x, y, 0))
            ),
            new THREE.LineBasicMaterial({ color: "#ffd166" })
        );

        Cube.userData.Color = "#55aaff";
        Orbit.userData.Color = "#ffd166";
        Group.add(Cube, Orbit);
        Scene.add(Group);
        Camera.position.z = 5;

        return { Camera, Group, Scene };
    }, [ ]);

    React.useEffect(() =>
    {
        Example.Camera.aspect = Width / Height;
        Example.Camera.updateProjectionMatrix();
    }, [ Example, Height, Width ]);

    React.useEffect(() =>
    {
        const Interval = setInterval(() =>
        {
            Example.Group.rotation.x += 0.015;
            Example.Group.rotation.y += 0.03;
        }, 1000 / 30);

        return () => clearInterval(Interval);
    }, [ Example ]);

    Ink.useInput((Input) =>
    {
        if (Input === "q")
        {
            exit();
        }
    });

    return (
        <Ink.Box flexDirection="column">
            <InkThreeView.InkThreeView
                Camera={ Example.Camera }
                Color="#66ccff"
                Fps={ 30 }
                Height={ Height }
                Lighting={{
                    AmbientIntensity: 0.2,
                    DiffuseIntensity: 0.8,
                    Direction: new THREE.Vector3(0.35, 0.6, 1)
                }}
                RenderColor
                RenderFaces
                RenderMode={ Terminal.RenderMode.RenderMode.Braille() }
                RenderWireframe
                Scene={ Example.Scene }
                Width={ Width }
            />
            <Ink.Text dimColor>q: quit</Ink.Text>
        </Ink.Box>
    );
}

Ink.render(<App />);

The component owns its redraw timer. Mutating a scene object does not require a React state update: the next redraw reads the current Three.js transforms. The application is still responsible for updating the camera aspect and projection matrix when its viewport changes.

Component options

InkThreeView.InkThreeView accepts the following props. Scene and Camera are required; the rest are optional.

| Prop | Default | Purpose | | --- | --- | --- | | Scene | — | The THREE.Scene to traverse. | | Camera | — | The THREE.Camera used to project geometry. | | Width | 80 | Output width in terminal cells. | | Height | 40 | Output height in terminal cells. | | Fps | 30 | How often the component reads the scene and redraws. | | RenderMode | Fixed() | Glyph encoder used for each terminal cell. | | Character | "█" | Glyph used by fixed mode. Only the first character is used. | | CharacterRamp | " ░▒▓█" | Coverage ramp used by shade mode. | | RenderFaces | false | Rasterizes mesh triangle interiors. | | RenderWireframe | !RenderFaces | Draws line objects and mesh feature edges. | | Lighting | enabled flat light | Controls face brightness. | | Color | "#66CCFF" | Fallback color when an object supplies no color. | | ColorOptions | see Color | Controls object and material color selection. | | RenderColor | false | Emits colored Ink spans instead of one plain text string. | | AutoResize | false | Uses stdout.columns and stdout.rows when available. |

AutoResize changes the renderer dimensions, but it does not change the Three.js camera. For a perspective camera, update Camera.aspect and call Camera.updateProjectionMatrix() in the application as shown above.

Render modes

Render modes are tagged values created through Terminal.RenderMode.RenderMode.

| Mode | Subcells per character | Best suited to | | --- | --- | --- | | Fixed() | 1 × 1 | Bold, low-resolution output or custom single-character drawing. | | Shade() | 2 × 4 | Filled surfaces and coverage-based brightness ramps. | | Braille() | 2 × 4 | Fine wireframes and dense terminal scenes. | | Quadrant() | 2 × 2 | Unicode block output with balanced horizontal and vertical detail. | | HalfBlock() | 1 × 2 | Simple Unicode output with extra vertical resolution. |

For example:

<InkThreeView.InkThreeView
    Camera={ Camera }
    CharacterRamp=" .:-=+*#%@"
    Height={ 32 }
    RenderFaces
    RenderMode={ Terminal.RenderMode.RenderMode.Shade() }
    Scene={ Scene }
    Width={ 100 }
/>

Character applies to Fixed(). CharacterRamp applies to Shade(). Braille, quadrant, and half-block modes choose their glyphs from occupied subcells.

Unicode modes assume the output terminal can display their characters. The Ink component does not perform capability detection; the Effect runtime can enforce Unicode and truecolor requirements through Terminal.InkThreeTerminal.

Faces, wireframes, and lighting

The renderer can draw mesh faces, wireframes, or both:

<InkThreeView.InkThreeView
    Camera={ Camera }
    Height={ 30 }
    Lighting={{
        AmbientIntensity: 0.25,
        DiffuseIntensity: 0.75,
        Direction: new THREE.Vector3(-0.25, 0.5, 1)
    }}
    RenderFaces
    RenderMode={ Terminal.RenderMode.RenderMode.Braille() }
    RenderWireframe
    Scene={ Scene }
    Width={ 90 }
/>

Face rendering projects triangles into a supersampled coverage buffer. Each sample stores coverage, depth, brightness, and color, so nearer triangles occlude farther triangles. Lighting is a renderer-local flat light calculated from the triangle's world-space normal.

Lighting options are:

  • Enabled, which defaults to true. Disabled lighting gives faces full brightness.
  • Direction, which defaults to (0.35, 0.6, 1) and is normalized internally.
  • AmbientIntensity, which defaults to 0.25 and is clamped from 0 to 1.
  • DiffuseIntensity, which defaults to 0.75 and is clamped from 0 to 1.

When RenderFaces is false, wireframes are on by default. When faces are on, wireframes are off unless RenderWireframe is explicitly enabled. A wireframe overlay receives a small depth bias so visible edges sit over their faces.

Color

Terminal color is separate from glyph selection. Set RenderColor on the Ink component to render colored spans:

const Mesh = new THREE.Mesh(
    new THREE.TorusKnotGeometry(1, 0.3, 80, 12),
    new THREE.MeshBasicMaterial({ color: "#44dd88" })
);

Mesh.userData.Color = "#ff8844";

<InkThreeView.InkThreeView
    Camera={ Camera }
    Color="#66ccff"
    ColorOptions={{
        Enabled: true,
        UseMaterialColor: true
    }}
    Height={ 32 }
    RenderColor
    RenderFaces
    Scene={ Scene }
    Width={ 100 }
/>

The renderer resolves an object's color in this order:

  1. A six-digit hex color in Object.userData.Color.
  2. The first material's Three.js color, when the object is a mesh and UseMaterialColor is enabled.
  3. ColorOptions.DefaultColor, then the top-level Color option, then the package default #66CCFF.

ColorOptions.Enabled and ColorOptions.UseMaterialColor both default to true. Setting Enabled to false uses the configured fallback for every object. Lighting scales the selected base color by the cell brightness.

The pure renderer always preserves color metadata in a structured frame. SceneToString intentionally returns only glyphs.

Pure rendering

Use Terminal.Renderer when the viewport is already known and rendering should be a deterministic synchronous operation.

import * as THREE from "three";
import { Terminal } from "@sorrell/ink-three";

const Scene = new THREE.Scene();
const Camera = new THREE.PerspectiveCamera(60, 80 / 24, 0.1, 100);
const Cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: "#5fd7ff" })
);

Camera.position.z = 4;
Scene.add(Cube);

const Frame = Terminal.Renderer.SceneToFrame(Scene, Camera, {
    Height: 24,
    RenderFaces: true,
    RenderMode: Terminal.RenderMode.RenderMode.Braille(),
    RenderWireframe: true,
    Width: 80
});

const Text = Terminal.Frame.ToString(Frame);

A frame contains an array of lines. Each cell has a Character, a normalized Brightness, and an optional RGB Color. This is the stable data model to use when testing renderer output or writing a custom terminal adapter.

To produce plain text directly:

const Text = Terminal.Renderer.SceneToString(Scene, Camera, {
    CharacterRamp: " .oO@",
    Height: 24,
    RenderFaces: true,
    RenderMode: Terminal.RenderMode.RenderMode.Shade(),
    Width: 80
});

The pure renderer also accepts AspectRatioCorrection, which defaults to 2 to compensate for terminal cells usually being taller than they are wide. A larger value narrows the projected scene horizontally; 1 disables the correction.

Effect rendering

The Effect API adds validated inputs, typed failures, replaceable services, and managed animation scheduling around the pure renderer.

Render one frame

import * as Effect from "effect/Effect";
import {
    SceneRenderer,
    Terminal
} from "@sorrell/ink-three";

const Program = SceneRenderer.RenderSceneToFrameEffect({
    Camera,
    Options: {
        RenderFaces: true,
        RenderMode: Terminal.RenderMode.RenderMode.Braille()
    },
    Scene,
    Viewport: {
        Height: 32,
        Width: 100
    }
}).pipe(
    Effect.provide(SceneRenderer.SceneRendererLive)
);

const Frame = await Effect.runPromise(Program);

RenderSceneToStringEffect accepts the same input and returns plain text. SceneRenderer.SceneRenderer is a Context.Service, so tests can provide a layer that returns fixture frames without traversing a Three.js scene.

Run an interruptible render loop

RendererRuntime.RunSceneRenderLoop reads the viewport from the terminal service, renders through the scene renderer service, and writes each frame to a frame sink. MakeDefaultRendererLayer provides the standard implementations.

import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import {
    RendererRuntime,
    Terminal
} from "@sorrell/ink-three";

const Live = RendererRuntime.MakeDefaultRendererLayer(
    () => ({ Height: 30, Width: 90 }),
    (Frame) =>
    {
        const Text = Terminal.Frame.ToString(Frame);
        process.stdout.write(`\u001B[H${ Text }`);
    },
    {
        TrueColor: true,
        Unicode: true
    }
);

const RenderLoop = RendererRuntime.RunSceneRenderLoop({
    Animation: { Fps: 30 },
    Camera,
    RendererOptions: {
        Color: "#66ccff",
        RenderFaces: true,
        RenderMode: Terminal.RenderMode.RenderMode.Braille(),
        RenderWireframe: true
    },
    Scene
});

const Main = Effect.gen(function*()
{
    const RenderFiber = yield* RenderLoop.pipe(Effect.forkChild);

    yield* Effect.sleep("5 seconds");
    yield* Fiber.interrupt(RenderFiber);
}).pipe(
    Effect.provide(Live)
);

await Effect.runPromise(Main);

The render loop reads current scene transforms on each tick. Scene mutation is owned by the application; the loop handles render timing, interruption, and the service boundaries. In an Ink application, prefer InkThreeView unless the services or a custom frame sink are useful to the larger program.

Services and layers

The runtime is composed from four services:

| Module | Service | Standard provider | | --- | --- | --- | | SceneRenderer | SceneRenderer.SceneRenderer | SceneRenderer.SceneRendererLive | | Terminal | Terminal.InkThreeTerminal | Terminal.MakeInkThreeTerminalLayer | | FrameSink | FrameSink.FrameSink | FrameSink.MakeFrameSinkLayer or MakeStringFrameSinkLayer | | AnimationDriver | AnimationDriver.AnimationDriver | AnimationDriver.AnimationDriverLive |

Provide these individually when a test or host application needs different behavior. For example, a test can supply a fixed viewport and collect frames in memory:

import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import {
    FrameSink,
    RendererRuntime,
    SceneRenderer,
    Terminal
} from "@sorrell/ink-three";

const Frames: Array<Terminal.Frame.Frame> = [ ];

const TestLayer = Layer.mergeAll(
    SceneRenderer.SceneRendererLive,
    FrameSink.MakeFrameSinkLayer((Frame) => Frames.push(Frame)),
    Terminal.MakeInkThreeTerminalLayer(
        () => ({ Height: 12, Width: 40 }),
        { TrueColor: true, Unicode: true }
    )
);

const TestProgram = Effect.gen(function*()
{
    const Frame = yield* RendererRuntime.RenderCurrentFrame(Scene, Camera);
    const Sink = yield* FrameSink.FrameSink;

    yield* Sink.WriteFrame(Frame);
}).pipe(Effect.provide(TestLayer));

await Effect.runPromise(TestProgram);

Validation and typed errors

Viewport.ValidateViewport parses an unknown value and validates positive integer dimensions:

import * as Effect from "effect/Effect";
import {
    InkThreeError,
    Viewport
} from "@sorrell/ink-three";

const Program = Viewport.ValidateViewport({
    Height: 24,
    Width: 0
}).pipe(
    Effect.catchTag("InvalidViewport", (Error: InkThreeError.InvalidViewport) =>
        Effect.succeed(`Invalid viewport: ${ Error.Message }`)
    )
);

Expected runtime failures are exported from InkThreeError:

| Error | Meaning | | --- | --- | | InvalidRenderOptions | Options could not be parsed or failed a domain check. | | InvalidViewport | Width or height is not a positive integer. | | UnsupportedTerminal | A requested Unicode or truecolor capability is unavailable. | | SceneRenderFailed | Scene traversal or rasterization failed. | | FrameCompositionFailed | A frame sink rejected a frame. | | AnimationLoopFailed | A render loop tick failed unexpectedly. | | InkRenderFailed | An Ink render lifecycle operation failed. |

These are tagged Effect errors rather than thrown control-flow exceptions. Terminal.MakeInkThreeTerminalLayer accepts explicit Unicode and TrueColor capabilities. The runtime requires Unicode for braille, quadrant, and half-block modes, and requires truecolor when a top-level render color is requested.

Supported Three.js input

The scene traversal respects Three.js world transforms, nested groups, and object visibility. The renderer reads:

  • THREE.Line and THREE.LineSegments backed by BufferGeometry position attributes.
  • THREE.Mesh backed by indexed or non-indexed triangle BufferGeometry.
  • BufferAttribute and InterleavedBufferAttribute position data with at least three components.
  • Any Three.js camera compatible with Vector3.project; the examples use PerspectiveCamera.

For wireframe meshes, shared coplanar triangle edges are removed so a box or other triangulated surface displays its feature edges rather than every internal diagonal. Unsupported object types and geometry without usable positions are skipped.

Package modules

The root entry point uses namespaces to keep the public API grouped by purpose:

| Namespace | Contents | | --- | --- | | InkThreeView | Ink component and its props. | | Terminal | Pure renderer, frame and color helpers, render modes, character encoders, and terminal service. | | SceneRenderer | Effect scene-renderer service and one-frame workflows. | | RendererRuntime | Current-frame rendering, render loops, and the default layer bundle. | | AnimationDriver | Animation options, validation, service, and live layer. | | FrameSink | Structured-frame and string sink layers. | | Viewport | Viewport model, schema, and validation. | | InkThreeError | Tagged renderer errors. | | Geometry | Edge and triangle extraction helpers. | | CharacterBuffer | Mutable character-grid helpers. | | CoverageBuffer | Coverage, depth, brightness, color, and line-rasterization helpers. |

Most applications only need InkThreeView and Terminal. The buffer and geometry namespaces are useful for custom renderers, encoders, and focused renderer tests.

Lower-level helpers

The lower-level modules are public for applications that need to build or adapt frames without using the complete scene renderer:

  • Terminal.Frame.ToString joins a structured frame into newline-delimited glyphs.
  • Terminal.Color includes ParseColor, ColorFromThreeColor, ScaleColor, ColorToHex, AverageColors, and DefaultColor.
  • Terminal.CharacterEncoder.GetCharacterEncoder returns the encoder for a tagged render mode. Encoders expose their subcell dimensions and can produce either a frame or text from a coverage buffer.
  • Geometry.ExtractEdges and Geometry.ExtractTriangles return local-space primitives from supported Three.js objects.
  • CharacterBuffer provides MakeCharacterBuffer, ClearCharacterBuffer, SetCharacter, DrawCharacterLine, and CharacterBufferToString for a mutable fixed-size glyph grid.
  • CoverageBuffer provides MakeCoverageBuffer, ClearCoverageBuffer, AddCoverage, GetCoverage, GetIntensity, GetColor, and DrawCoverageLine for raster samples with depth and color metadata.
  • RendererRuntime.MakeSceneRenderInput packages a scene, camera, viewport, and fixed renderer options into a SceneRenderer input.

Character-buffer and coverage-buffer operations support both direct and pipe-friendly call forms. Coordinates outside a buffer are ignored by write operations and return empty values from reads.

Renderer boundaries

The package is a CPU terminal renderer, not a Three.js WebGL renderer. It uses Three.js scene data and projection math but does not execute shaders or use Three.js renderer, light, or texture pipelines.

Current rendering behavior is deliberately narrower than browser Three.js:

  • Face color comes from userData.Color, a material's basic color property, or a configured fallback. Textures, transparency, blending, and material shaders are not evaluated.
  • Lighting is one renderer-local directional flat light. Three.js light objects do not affect the frame.
  • Mesh skinning, morph targets, and per-instance transforms are not applied.
  • Clipping is conservative rather than a full homogeneous clip-plane pass, so geometry crossing extreme camera bounds may be omitted.
  • Larger viewports and supersampled modes perform more CPU raster work. Fixed mode uses one sample per cell; braille and shade use eight.

These boundaries keep the output deterministic and make frames usable in Ink, plain-text output, snapshots, and custom terminal sinks.

Demo and development

Run the repository demo:

npm run demo

Use Left and Right to cycle render modes. Press q or Ctrl+C to exit.

Repository checks are:

npm run check
npm run lint
npm test
npm run build

License

MIT