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

slate-motion

v0.3.0

Published

A cinematic chalkboard-style HTML content reveal renderer for React

Readme

InkFrame

Cinematic Word-by-Word Chalkboard HTML Presentation Renderer for React

InkFrame is a high-performance React component designed to render HTML content word-by-word with the visual feel of a cinematic classroom chalkboard. It includes automated and audio-synchronized timelines, realistic drawing cursors with fluid motion, custom layers, and an integrated HUD control panel.


Table of Contents

  1. Key Features
  2. Architecture Overview
  3. Running Locally & Development
  4. Getting Ready for npm Package
  5. Publishing to npm
  6. Component API Reference
  7. Imperative Handle API
  8. Advanced Architecture Guidelines
  9. License

Key Features

  • 🎬 Cinematic Aesthetics: Authentic dark chalkboard texture with a customizable grid, soft vignettes, custom cursive/code fonts, and smooth fades.
  • Zero-React 60fps Tick Loop: Animations, cursor movements, and word opacity transitions bypass React state entirely, writing direct styling to DOM elements inside a requestAnimationFrame (rAF) loop for stutter-free playback.
  • 🎙️ Sub-millisecond Audio Sync: Synchronize word-by-word reveal timings to actual audio tracks using the Web Audio API with automatic drift correction.
  • ✏️ Natural Cursor Motion: Realistic cursor types (hand, chalk, marker, pen) with natural sway, pivot adjustments, and container-relative coordinate calculations to eliminate cursor drift.
  • 🧬 Rich Media Revelations: Renders images, inline SVGs, and KaTeX math formulas at the exact moment they are reached in the text timeline.
  • 🎛️ Playable HUD Controls: Integrated responsive playback scrubber, status indicator, play/pause controls, and playback speed triggers.

Architecture Overview

InkFrame stacks 5 independent rendering layers within a single relative container element to isolate rendering concerns and optimize GPU performance:

<ChalkboardRenderer>  (Relative outer container, fixed viewport dimensions)
│
├── <BackgroundLayer>  (Canvas 2D, drawn once on initialization)
├── <ContentLayer>     (Pre-renders words at opacity:0; registers DOM nodes)
├── <MediaLayer>       (Absolute overlays for images, SVGs, and CDN KaTeX formulas)
├── <CursorLayer>      (Absolutely positioned single element for pointer styling)
└── <HUDLayer>         (React DOM controls, throttled to 10fps for scrubber & timing updates)

Running Locally & Development

1. Installation

Ensure you have Node.js (v18+) installed. Clone the repository and install dependencies:

# Using npm
npm install

# Using yarn
yarn install

2. Start the Local Playground / Dev Server

To run the demo sandbox locally (which renders the interactive lessons in src/App.tsx):

npm run dev

This boots Vite and opens the local playground at http://localhost:5173.

3. Build & Preview the Demo Site

To build and preview the standalone static application:

# Build the React/Vite playground
npm run build

# Preview the local build
npm run preview

4. Running Lints & Type Checks

To check for code cleanliness and run strict type checks:

# Run ESLint check
npm run lint

# Run strict TypeScript compilation
# NOTE: Avoid using global "npx tsc" as it may resolve a wrong package. Use the project path:
node node_modules/typescript/bin/tsc --noEmit --strict

Getting Ready for npm Package

InkFrame is prepared to be compiled and shipped as a lightweight, clean, and self-contained npm package. The package files and export pipelines are pre-configured inside package.json, vite.config.ts, and tsconfig.lib.json.

Library Build Process

Vite bundles the library code and TypeScript generates the typings when running:

npm run build:lib

This triggers the following events:

  1. JavaScript Bundling: vite build uses the library config (src/chalkboard/index.ts as entry) to create two files in dist/:
    • dist/inkframe.es.js (ES Module format for bundlers like Webpack, Vite, Next.js)
    • dist/inkframe.cjs.js (CommonJS module for traditional setups)
    • Note: React, React-DOM, and React JSX Runtimes are excluded from the bundle (externalized) so that the user's React runtime is reused.
  2. TypeScript Declarations: tsc -p tsconfig.lib.json compiles the chalkboard folder and emits types definitions (index.d.ts and related .d.ts files) directly into dist/.

Package Configurations

The package defines its public API via standard fields in package.json:

  • Exports Barrel: Points to both CommonJS and ES formats, along with types declarations.
    "main": "./dist/inkframe.cjs.js",
    "module": "./dist/inkframe.es.js",
    "types": "./dist/index.d.ts",
    "exports": {
      ".": {
        "import": "./dist/inkframe.es.js",
        "require": "./dist/inkframe.cjs.js",
        "types": "./dist/index.d.ts"
      }
    }
  • Included Files: Only the dist directory is shipped, keeping the package size minimal.
    "files": [
      "dist"
    ]
  • Npm Ignores: .npmignore excludes development directories, config files, and TS configs from being packed:
    src/
    public/
    *.config.ts
    *.config.js
    tsconfig*.json
    .eslintrc*
    eslint.config*
    index.html

Publishing to npm

To publish InkFrame to the public npm registry, follow these step-by-step instructions:

Step 1: Automated Library Build Verification

Before publishing, ensure the library builds locally without any compilation or typing issues:

npm run build:lib

Step 2: Version Bump

Determine if the release is a patch, minor, or major update and bump the version inside package.json. This will also create a corresponding git version tag:

# Bumps 0.1.0 to 0.1.1 (Bug fixes, doc improvements)
npm version patch

# Bumps 0.1.0 to 0.2.0 (New features, backward-compatible API changes)
npm version minor

# Bumps 0.1.0 to 1.0.0 (Breaking changes or major launches)
npm version major

Step 3: Dry-Run Verification

Verify exactly which files will be packaged and uploaded to the registry to ensure no unwanted configuration files or source code leakage occurs:

npm publish --dry-run

Confirm that only package.json, README.md, and the compiled dist/ files are listed in the tarball.

Step 4: Login to npm

Log into your npm account through the terminal command line (only needed once):

npm login

Step 5: Publish Package

Publish the library to the npm registry. Since the prepublishOnly lifecycle hook is configured, npm will automatically re-compile the library right before uploading:

# If your package has public access settings
npm publish --access public

Component API Reference

Install InkFrame in your React application:

npm install slate-motion

Example Usage

import React, { useRef } from 'react';
import { ChalkboardRenderer } from 'slate-motion';
import type { ChalkboardHandle } from 'slate-motion';

const LESSON_HTML = `
  <h2>Solving equations</h2>
  <p>To solve for <strong>x</strong>, we subtract 3 from both sides:</p>
  <div class="math">x + 3 = 10</div>
  <p>Therefore, we get <strong>x = 7</strong>.</p>
`;

export default function MathLesson() {
  const chalkboardRef = useRef<ChalkboardHandle>(null);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
      <ChalkboardRenderer
        ref={chalkboardRef}
        html={LESSON_HTML}
        width={1100}
        height={620}
        theme="dark"
        cursorStyle="chalk"
        wpm={150}
        showHUD={true}
        onWordReveal={(id, word) => console.log(`Writing: ${word}`)}
        onComplete={() => alert('Lesson completed!')}
      />
      
      <div style={{ display: 'flex', gap: '8px' }}>
        <button onClick={() => chalkboardRef.current?.play()}>Play</button>
        <button onClick={() => chalkboardRef.current?.pause()}>Pause</button>
        <button onClick={() => chalkboardRef.current?.reset()}>Restart</button>
      </div>
    </div>
  );
}

Props Reference

| Prop Name | Type | Default | Description | | :--- | :--- | :--- | :--- | | html | string | (Required) | The trusted input HTML content containing text blocks, headings, lists, inline SVGs, images, or KaTeX math classes. | | audio | { src?: string; buffer?: ArrayBuffer } | undefined | Optional audio narration source. Uses Web Audio API for playback speed-adjusted synchronization. | | timings | WordTiming[] | [] | Explicit word-level timestamp alignments matching the audio timeline. Format: [{ word: string, start: number, end: number }]. | | width | number | 1280 | Renderer viewport width in pixels. | | height | number | 720 | Renderer viewport height in pixels. | | theme | 'dark' \| 'light' | 'dark' | Visual skin of the chalkboard. 'dark' gives a black slate look; 'light' replicates a classic physical whiteboard. | | cursorStyle | 'chalk' \| 'marker' \| 'pen' \| 'hand' | 'chalk' | The active drawing pointer graphic displayed over the revealing text. | | autoplay | boolean | false | If true, the chalkboard starts rendering text immediately upon layout measurement completion. | | wpm / wordsPerMinute | number | 130 | Playback speed fallback used when no explicit audio timings are provided. | | fadeDelay | number | 3.5 | Delay in seconds after the final word of a paragraph block is rendered before that block starts to fade out. | | fadeDuration | number | 1.4 | Duration in seconds it takes for a fading paragraph block to become fully transparent. | | contentPadding | { top: number, right: number, bottom: number, left: number } | { top: 64, right: 96, bottom: 64, left: 96 } | Inner border boundaries in pixels where text blocks are wrapped. | | showHUD | boolean | true | Toggles whether the bottom media progress bar, play/pause controls, timer, and speed multiplier HUD are visible. | | config | Partial<ChalkboardConfig> | undefined | Legacy support to feed a structured config object directly. | | onWordReveal | (wordId: string, word: string) => void | undefined | Callback invoked as the writing pointer starts to construct a specific word span. | | onProgress | (currentTime: number, duration: number) => void | undefined | Callback invoked on each rendering frame with current playback time and total timeline duration. | | onComplete | () => void | undefined | Callback triggered when the timeline reaches its final frame. |


Imperative Handle API

By passing a React ref to <ChalkboardRenderer>, you obtain direct, synchronous control over the animation engine via the ChalkboardHandle interface.

Available Ref Commands

  • play(): void Resumes execution of the animation timeline. Automatically resumes associated audio narration if present.
  • pause(): void Pauses the chalkboard animation loop and narration.
  • seek(seconds: number): void Seeks to a specific playback time in seconds. Recomputes word opacities and moves the cursor immediately to the corrected coordinate.
  • setSpeed(rate: number): void Sets the playback speed rate multiplier (e.g. 0.5 to 2.0). Affects both text speed and audio narration speed dynamically.
  • reset(): void Resets the entire session back to 0 seconds, putting the cursor at its initial position and hiding all word layers.
  • getState(): PlaybackState Retrieves a snapshot of the current state: { status: PlaybackStatus, duration: number, currentTime: number, speed: number }.
  • getCurrentTime(): number Returns the exact current playback coordinate in seconds.

Advanced Architecture Guidelines

When extending or editing the internal core layers, keep these critical guidelines in mind to avoid regressions:

1. StrictMode Double-Mount Handling

React 18's StrictMode double-mounts components in development to identify side-effects. In the ContentLayer.tsx measurement effect, a stale measuredRef guard would block on the second mount, causing the canvas to freeze forever on "preparing...".

  • Rule: Do not add a measuredRef guard back to ContentLayer. The cleanup function cancels the previous frame ID (cancelAnimationFrame(frameId)), which is sufficient deduplication to allow the second layout pass to measure coordinates correctly.

2. Stable Measurement Callback (handleMeasured)

The handleMeasured callback passed from the main component must remain stable. If its reference changes on subsequent renders, it causes ContentLayer's measurement hook to fire repeatedly.

  • Rule: Maintain the measureCtxRef pattern inside ChalkboardRenderer.tsx. Always copy current parameters to the mutable ref on render and execute inside a dependencies-free useCallback([], ...) block.

3. Container-Relative Cursor Coordinates

getBoundingClientRect() returns coordinates relative to the screen's viewport. Using these values directly inside CSS translate transitions causes the cursor to drift as soon as the container moves on the page.

  • Rule: On initialization, the origin of the outermost container is subtracted from the viewport coordinates to map words into safe, absolute relative positions:
    const origin = containerEl?.getBoundingClientRect();
    const ox = origin?.left ?? 0;
    const oy = origin?.top ?? 0;
    token.position = new DOMRect(r.x - ox, r.y - oy, r.width, r.height);

4. Interactive Audio Gestures

Modern browsers block the startup of audio pipelines until an explicit user gesture is registered. Calling play() programmatically on page load without user input will lead to a suspended state.

  • Rule: Prior to running programmatic calls, execute audio.ensureContext() which yields to existing contexts or wakes them up during the initial button interaction.

License

MIT © Mukesh Seenivasan