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

tee-skin-renderer

v0.3.2

Published

<p align="center"><b>tee-skin-renderer</b> - Teeworlds / DDNet skin renderer for you HTML pages</p>

Downloads

683

Readme


Getting Started

Usage (UMD)

<head>
    <!-- ... -->
    <link rel="preload" as="image" href="https://ddstats.tw/skins/default.png">
    <link rel="stylesheet" href="https://unpkg.com/tee-skin-renderer/dist/tee-skin-renderer.css">
    <!-- ... -->
</head>

<body>
    <!-- Basic skin -->
    <div
        class="tee"
        data-skin="https://skins.scrumplex.net/skin/pinky.png"
    >
    </div>

    <!-- Custom colors -->
    <div
        class="tee"
        data-skin="https://skins.scrumplex.net/skin/pinky.png"
        data-color-body="5498880"
        data-color-feet="3079936"
    >
    </div>

    <!-- All options -->
    <div
        class="tee"
        data-skin="https://skins.scrumplex.net/skin/pinky.png"
        data-color-body="5498880"
        data-color-feet="3079936"
        data-use-custom-color="true"
        data-eyes="happy"
        data-speed="10"
        data-fat="true"
        data-follow-mouse="true"
    >
    </div>

    <script src="https://unpkg.com/tee-skin-renderer/dist/tee-skin-renderer.umd.js"></script>
</body>

Usage (ESM)

npm install tee-skin-renderer --save
import { createAsync } from 'tee-skin-renderer';
import 'tee-skin-renderer/css';

createAsync({
    skinUrl: 'https://skins.scrumplex.net/skin/pinky.png',
    colorBody: 5498880,
    colorFeet: 3079936,
    eyes: 'happy',
    speed: 10,
    inAir: false,
    fat: false,
    followMouse: true,
}).then((container) => {
    document.body.appendChild(container);
});
import { init } from 'tee-skin-renderer';
import 'tee-skin-renderer/css';

init();

Sizing

The tee container is 96em × 96em. Since .tee sets font-size: 1px by default, this equals 96 × 96 px. Change font-size to scale the tee:

/* CSS classes */
.tee-xs { font-size: 0.35px; }  /* ~34px */
.tee-sm { font-size: 0.55px; }  /* ~53px */
.tee-md { font-size: 1px;    }  /* 96px (default) */
.tee-lg { font-size: 1.6px;  }  /* ~154px */
.tee-xl { font-size: 2.2px;  }  /* ~211px */
<!-- Inline -->
<div class="tee" style="font-size: 1.5px" data-skin="..."></div>
// Programmatic
container.style.fontSize = '2px';

Animation

The renderer uses the same DDNet animation keyframes for idle, walking, running, AFK and in-air poses.

speed is the signed horizontal velocity in DDNet world units per tick. It controls both the animation mode and the cycle phase:

  • 0 (or |speed| <= 1 / 256) — idle;
  • 0 < |speed| < 5000 / 256 — walk;
  • |speed| >= 5000 / 256 — run;
  • negative speed plays the leg cycle backwards while the tee keeps facing right;
  • inAir: true — jump/fall pose, which takes priority over the movement animation.

The animation is advanced with requestAnimationFrame, using DDNet's 50 ticks per second.

const container = await createAsync({
    skinUrl: 'https://ddstats.tw/skins/pinky.png',
    speed: 10, // walk
});

const tee = container.tee;
tee.speed = 20;  // run forward
tee.speed = -20; // run with the reverse leg phase
tee.inAir = true; // jump/fall pose
tee.inAir = false;

Custom animations

Custom animations are programmatic and temporarily replace the visible DDNet pose. The built-in idle, walk, run, sit and inAir animations are fixed and cannot be redefined. Create a separate animation when you need different behavior.

While a custom animation is visible, the built-in movement phase keeps advancing in the background. Stopping the custom animation therefore returns the tee to the current DDNet pose without restarting its walk or run cycle.

Keyframe animations

Define a reusable keyframe animation and play it on any tee:

import { createAsync, defineAnimation } from 'tee-skin-renderer';

const bounce = defineAnimation({
    kind: 'keyframes',
    name: 'bounce',
    duration: 900,
    loop: true,
    easing: 'ease-in-out',
    tracks: {
        body: [
            { time: 0, y: 0 },
            { time: 0.2, y: 0 },
            { time: 0.5, y: -6 },
            { time: 0.8, y: 0 },
            { time: 1, y: 0 },
        ],
        backFoot: [
            { time: 0, x: -7, y: 0, angle: 0 },
            { time: 0.2, x: -7, y: 0, angle: 0 },
            { time: 0.5, x: -6, y: -4, angle: -0.05 },
            { time: 0.8, x: -7, y: 0, angle: 0 },
            { time: 1, x: -7, y: 0, angle: 0 },
        ],
        frontFoot: [
            { time: 0, x: 7, y: 0, angle: 0 },
            { time: 0.2, x: 7, y: 0, angle: 0 },
            { time: 0.5, x: 6, y: -4, angle: 0.05 },
            { time: 0.8, x: 7, y: 0, angle: 0 },
            { time: 1, x: 7, y: 0, angle: 0 },
        ],
        eyes: [{ time: 0, eyes: 'happy' }],
    },
});

const container = await createAsync({
    skinUrl: 'https://ddstats.tw/skins/pinky.png',
});
const playback = container.tee.playAnimation(bounce);

playback.pause();
playback.resume();
playback.seek(400); // milliseconds
playback.stop();

const result = await playback.finished;
console.log(result.reason); // "stopped"

Procedural animations

For procedural motion, return a pose for each frame. The callback receives animation time plus the tee's current movement state:

const hop = defineAnimation({
    kind: 'callback',
    name: 'hop',
    duration: 900,
    loop: true,
    frame({ progress, speed, inAir }) {
        const lift = Math.sin(progress * Math.PI) ** 2;

        return {
            body: { y: -5 * lift },
            backFoot: { x: -7 + lift, y: -3 * lift, angle: -0.04 * lift },
            frontFoot: { x: 7 - lift, y: -3 * lift, angle: 0.04 * lift },
            eyes: inAir || Math.abs(speed) >= 5000 / 256
                ? 'surprise'
                : 'happy',
        };
    },
});

container.tee.playAnimation(hop, {
    playbackRate: 1,
    startAt: 0,
});

UMD usage

Use TeeSkinRenderer.animation.define(...) in a script build. TeeSkinRenderer.defineAnimation(...) is an equivalent direct export.

<link rel="stylesheet" href="https://unpkg.com/tee-skin-renderer/dist/tee-skin-renderer.css">
<script src="https://unpkg.com/tee-skin-renderer/dist/tee-skin-renderer.umd.js"></script>
<script>
    const blink = TeeSkinRenderer.animation.define({
        kind: 'keyframes',
        duration: 300,
        tracks: {
            eyes: [
                { time: 0, eyes: 'normal' },
                { time: 0.5, eyes: 'blink' },
                { time: 1, eyes: 'normal' },
            ],
        },
    });

    TeeSkinRenderer.createAsync({
        skinUrl: 'https://ddstats.tw/skins/pinky.png',
    }).then((container) => {
        document.body.appendChild(container);
        container.tee.playAnimation(blink);
    });
</script>

Custom animations do not have a data attribute or global name registry. Keep the returned definition in your application and pass it directly to playAnimation().

Definition reference

Every animation definition supports these fields:

| Field | Type | Default | Description | |---|---|---|---| | kind | 'keyframes' \| 'callback' | required | Selects keyframe tracks or a procedural callback | | name | string | — | Optional label for debugging and UI; it does not register or override an animation | | duration | number | required | Duration of one iteration in milliseconds; must be greater than zero | | loop | boolean | false | Repeats the animation until stopped | | fill | 'none' \| 'forwards' | 'none' | Restores the DDNet pose or holds the final custom pose after completion |

Keyframe definitions additionally accept tracks and an animation-wide easing. Tracks are independent:

| Track | Keyframe shape | Description | |---|---|---| | body | { time, x?, y?, angle?, scale?, easing? } | Body transform | | backFoot | { time, x?, y?, angle?, scale?, easing? } | Back foot transform | | frontFoot | { time, x?, y?, angle?, scale?, easing? } | Front foot transform | | eyes | { time, eyes } | Discrete eye-type changes |

time is normalized from 0 to 1. Keyframes may be supplied in any order, but duplicate times are invalid. A single keyframe or values outside the first/last keyframe are held without interpolation.

Transform values are offsets from the neutral tee pose:

| Field | Neutral value | Unit | |---|---:|---| | x | 0 | DDNet's 64-unit tee coordinate space | | y | 0 | DDNet's 64-unit tee coordinate space | | angle | 0 | Turns; 0.25 equals 90 degrees | | scale | 1 | Multiplier |

Missing transform fields use their neutral value at each keyframe; values are not implicitly copied from the previous keyframe. Eye frames switch discretely and support normal, angry, pain, happy, dead, surprise and blink.

Animation-wide and per-keyframe easing supports linear, ease, ease-in, ease-out, ease-in-out, or [x1, y1, x2, y2]. A keyframe's easing controls interpolation from that keyframe to the next one.

Callback context

A callback definition uses frame(context) and returns the same { body, backFoot, frontFoot, eyes } pose shape:

| Context field | Description | |---|---| | progress | Current iteration progress from 0 to 1 | | elapsedMs | Total playback position in milliseconds, including completed loop iterations | | deltaMs | Playback-adjusted frame delta, capped at 100 ms | | iteration | Zero-based loop iteration | | speed | Current signed DDNet horizontal speed | | inAir | Current jump/fall state | | afk | Current AFK state |

If a callback throws or returns an invalid pose, playback ends with reason error and the renderer restores the built-in DDNet pose.

Playback options and controller

tee.playAnimation(definition, options?) starts an animation and returns a controller. Starting another custom animation replaces the previous one.

| Playback option | Default | Description | |---|---:|---| | loop | definition value | Overrides looping for this playback | | fill | definition value | Overrides none/forwards for this playback | | playbackRate | 1 | Positive timeline speed multiplier | | startAt | 0 | Initial timeline position in milliseconds |

| Controller member | Description | |---|---| | definition | Normalized, frozen animation definition | | playState | running, paused, finished or stopped | | currentTime | Current timeline position in milliseconds | | progress | Current iteration progress from 0 to 1 | | pause() / resume() | Pauses or continues timeline advancement | | seek(timeMs) | Moves playback to an absolute timeline position | | stop() | Stops playback and restores the built-in pose | | finished | Promise that always resolves with { reason, error? } |

Completion reasons are completed, stopped, replaced, destroyed and error. The promise never rejects. With fill: 'forwards', playState becomes finished while the final pose remains active; call stop(), tee.stopAnimation(), or start another animation to release it.

Skin Format

Supports standard Teeworlds/DDNet skin images with a 2:1 aspect ratio at any resolution: 256×128, 512×256, 1024×512, 2048×1024, etc.

Data Attributes

| Attribute | Type | Description | |---|---|---| | data-skin | string | Skin image URL (required) | | data-color-body | number | Body color in Teeworlds format | | data-color-feet | number | Feet color in Teeworlds format | | data-use-custom-color | boolean | Enable/disable custom coloring | | data-eyes | string | Eye type: normal, angry, pain, happy, dead, surprise, blink | | data-speed | number | Signed horizontal velocity in DDNet world units per tick; controls walk/run and phase | | data-in-air | boolean | Use the in-air/jump pose | | data-fat | boolean | Fat skin mode (1.3× body scale) | | data-afk | boolean | AFK state with sit pose and blink eyes | | data-follow-mouse | boolean | Eyes follow the mouse cursor; otherwise they look right like DDNet previews |

API

createAsync(config): Promise<TeeContainer>

Creates a tee renderer programmatically.

const container = await createAsync({
    skinUrl: 'https://ddstats.tw/skins/pinky.png',
    colorBody: 5498880,
    colorFeet: 3079936,
    useCustomColor: true,
    eyes: 'happy',
    speed: 10,
    inAir: false,
    fat: false,
    afk: false,
    followMouse: true,
});

document.body.appendChild(container);

init(simultaneously?: boolean): Promise<void>

Auto-initializes all .tee elements on the page. By default initializes all tees simultaneously; pass false to initialize sequentially.

TeeRenderer instance

Access the renderer via container.tee:

const container = await createAsync({ skinUrl: '...' });
const tee = container.tee;

// Properties (get/set)
tee.skinUrl = '...';           // Change skin
tee.colorBody = 5498880;       // Set body color (or undefined to clear)
tee.colorFeet = 3079936;       // Set feet color (or undefined to clear)
tee.useCustomColor = true;     // Toggle custom colors
tee.eyes = 'angry';            // Change eye type
tee.speed = -20;                // Reverse the leg animation phase
tee.inAir = true;               // Use the jump/fall pose
tee.fat = true;                // Toggle fat mode
tee.afk = true;                // Toggle AFK state
tee.followMouse = true;        // Toggle mouse following

// Read-only
tee.colorBodyHsl;              // [h, s, l] or undefined
tee.colorBodyRgba;             // [r, g, b, a] or undefined
tee.colorFeetHsl;
tee.colorFeetRgba;
tee.skinBitmap;                // ImageBitmap or null

// Methods
tee.update();                  // Force re-render
tee.destroy();                 // Clean up resources
const playback = tee.playAnimation(animationDefinition, {
    loop: true,
    fill: 'none',
    playbackRate: 1,
    startAt: 0,
});
tee.currentAnimation;          // Active custom controller or null
tee.stopAnimation();           // Stop custom animation and restore DDNet pose
tee.renderToCanvas(canvas, {   // Render to canvas element
    size: 128,                 //   output size in px (default: 96)
    eyes: 'happy',             //   override eye type
});

The playback controller exposes playState, currentTime, progress, pause(), resume(), seek(), stop() and a finished promise. finished always resolves with { reason }, where reason is completed, stopped, replaced, destroyed or error.

renderToCanvas uses the renderer's current built-in or custom animation frame and eye direction. Its eyes option has priority over custom animation eyes.

Events

container.tee.addEventListener('tee:skin-loaded', (e) => {
    const { skin, success } = e.detail.payload;
    console.log(`Skin ${skin}: ${success ? 'loaded' : 'failed'}`);
});

container.tee.addEventListener('tee:rendered', (e) => {
    console.log('Tee rendered!');
});

Color Utilities

import { color } from 'tee-skin-renderer';

color.convertTeeColorToHsl(5498880);   // [h, s, l]
color.convertTeeColorToRgba(5498880);  // [r, g, b, a]
color.convertHslToRgba([120, 100, 75]); // [r, g, b, a]

License

tee-skin-renderer is licensed under a CC0-1.0 License.