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

@ksteinstudio/game-controller

v1.0.1

Published

Universal Game Controller Engine - render interactive UI layouts via JSON configuration in an Iframe-based SDK. Percentage-based positioning, multi-touch, zero dependencies.

Readme

Universal Game Controller Engine

npm version license

A lightweight, zero-dependency engine for rendering interactive virtual game controllers via JSON configuration. Renders inside an <iframe> and communicates with the parent window through postMessage. Perfect for mobile web games, browser-based emulators, and remote control interfaces.

Key features:

  • Percentage-based positioning (0–100) for resolution independence
  • Multi-touch support via PointerEvents API
  • JSON-driven layout — define controllers as data
  • Iframe-sandboxed renderer — secure and isolated
  • GPU-accelerated CSS transforms for low-latency input
  • Zero runtime dependencies
  • TypeScript-first with full type exports

Installation

# npm
npm install @ksteinstudio/game-controller

# yarn
yarn add @ksteinstudio/game-controller

# pnpm
pnpm add @ksteinstudio/game-controller
  • Works with any framework: React, Vue, Svelte, Angular, or vanilla JS/TS
  • Supports ESM and CommonJS
  • TypeScript types included out of the box
  • Browser-only (requires DOM APIs)

Quick Start

1. Define a Controller Layout (JSON)

{
  "version": "1.0.0",
  "name": "My-Layout",
  "canvas": {
    "aspectRatio": "16:9",
    "backgroundColor": "rgba(0,0,0,0.5)"
  },
  "elements": [
    {
      "id": "left-stick",
      "type": "joystick",
      "position": { "x": 15, "y": 60 },
      "zIndex": 1,
      "radius": 12,
      "innerStickSize": 5,
      "deadzone": 0.1,
      "mode": "static",
      "style": { "color": "rgba(85, 85, 85, 0.6)" }
    },
    {
      "id": "btn-a",
      "type": "button",
      "position": { "x": 85, "y": 65 },
      "zIndex": 1,
      "shape": "circle",
      "size": 8,
      "label": "A",
      "actionKey": "JOY_A",
      "style": { "color": "#4CAF50" }
    }
  ]
}

2. Embed with the SDK

import { createControllerSDK } from '@ksteinstudio/game-controller';
import layout from './controller.json';

const controller = createControllerSDK({
  config: layout,
  container: document.getElementById('controller-container'),
  onInput: (event) => {
    console.log('Input:', event.type, event.payload);
  },
  onReady: () => {
    console.log('Controller is ready');
  },
});

// Update the layout dynamically
controller.updateConfig(newLayout);

// Clean up
controller.destroy();

API Reference

createControllerSDK(options)

Creates and embeds a controller inside a container element.

| Option | Type | Required | Description | |--------|------|----------|-------------| | config | ControllerConfig | ✅ | The JSON layout configuration | | container | HTMLElement | ✅ | DOM element to mount the iframe into | | iframeSrc | string | ❌ | Custom URL for the renderer (uses embedded renderer by default) | | onInput | (event: InputEvent) => void | ❌ | Callback for all input events | | onReady | () => void | ❌ | Called when the renderer is initialized | | width | string | ❌ | CSS width for the iframe (default: 100%) | | height | string | ❌ | CSS height for the iframe (default: 100%) |

Returns ControllerSDKInstance:

| Method | Description | |--------|-------------| | updateConfig(config) | Send a new layout to the renderer | | destroy() | Remove the iframe and clean up listeners | | getIframe() | Access the underlying HTMLIFrameElement |


Type Definitions

Position

All positions use a percentage-based coordinate system (0–100) for resolution independence.

interface Position {
  x: number; // 0 to 100 (% of canvas width)
  y: number; // 0 to 100 (% of canvas height)
}

Element Types

| Type | Description | Key Properties | |------|-------------|----------------| | button | Pressable button (circle or square) | shape, size, label, actionKey | | joystick | Analog stick with normalized output | radius, innerStickSize, deadzone, mode | | dpad | 4-directional pad | size, actionKeys | | slider | Linear input control | length, orientation, actionKey |

Events Emitted

| Event | Payload | When | |-------|---------|------| | INPUT_START | { elementId, actionKey, timestamp } | Button pressed | | INPUT_END | { elementId, actionKey, timestamp } | Button released | | JOYSTICK_MOVE | { elementId, vector: {x,y}, angle, magnitude, timestamp } | Joystick moves (vector normalized -1 to 1) | | DPAD_PRESS | { elementId, direction, actionKey, timestamp } | DPad direction pressed | | DPAD_RELEASE | { elementId, direction, actionKey, timestamp } | DPad direction released |


Math Utilities

Exported utilities for building custom layout editors or tools.

Snap-to-Grid

import { snapToGrid, snapPositionToGrid } from '@ksteinstudio/game-controller';

const snapped = snapToGrid(37, 20); // → 35
const pos = snapPositionToGrid({ x: 37.2, y: 62.8 }, 20); // → { x: 35, y: 65 }

Coordinate Conversion

import { pixelToPercentage, percentageToPixel } from '@ksteinstudio/game-controller';

const pct = pixelToPercentage(320, 1920); // → 16.67
const px = percentageToPixel(50, 1080);   // → 540

Alignment Guides

import { findAlignmentGuides } from '@ksteinstudio/game-controller';

const result = findAlignmentGuides(draggedElement, cursorPosition, otherElements);
// result.position → snapped position
// result.guides   → array of active alignment lines

Framework Examples

React

import { useEffect, useRef } from 'react';
import { createControllerSDK, ControllerConfig, InputEvent } from '@ksteinstudio/game-controller';

const layout: ControllerConfig = {
  version: '1.0.0',
  name: 'react-controller',
  canvas: { aspectRatio: '16:9', backgroundColor: 'rgba(0,0,0,0.4)' },
  elements: [
    {
      id: 'stick', type: 'joystick', position: { x: 20, y: 65 },
      zIndex: 1, radius: 12, innerStickSize: 5, deadzone: 0.1, mode: 'static',
      style: { color: 'rgba(80,80,80,0.6)' },
    },
    {
      id: 'btn-a', type: 'button', position: { x: 85, y: 60 },
      zIndex: 1, shape: 'circle', size: 10, label: 'A', actionKey: 'JUMP',
      style: { color: '#4CAF50' },
    },
  ],
};

function GameController() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const sdk = createControllerSDK({
      config: layout,
      container: containerRef.current,
      onInput: (event: InputEvent) => {
        console.log(event.type, event.payload);
      },
    });

    return () => sdk.destroy();
  }, []);

  return <div ref={containerRef} style={{ width: '100%', height: '300px' }} />;
}

Vue 3

<template>
  <div ref="controllerRef" style="width: 100%; height: 300px" />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { createControllerSDK, type ControllerSDKInstance } from '@ksteinstudio/game-controller';

const controllerRef = ref<HTMLDivElement>();
let sdk: ControllerSDKInstance;

onMounted(() => {
  if (!controllerRef.value) return;

  sdk = createControllerSDK({
    config: {
      version: '1.0.0',
      name: 'vue-controller',
      canvas: { aspectRatio: '16:9' },
      elements: [
        {
          id: 'joy', type: 'joystick', position: { x: 20, y: 65 },
          zIndex: 1, radius: 12, innerStickSize: 5, deadzone: 0.1, mode: 'static',
        },
      ],
    },
    container: controllerRef.value,
    onInput: (event) => console.log(event),
  });
});

onUnmounted(() => sdk?.destroy());
</script>

Vanilla JavaScript (No Bundler)

<div id="controller" style="width: 100%; height: 400px;"></div>

<script type="module">
  import { createControllerSDK } from 'https://esm.sh/@ksteinstudio/game-controller';

  createControllerSDK({
    config: {
      version: '1.0.0',
      name: 'vanilla-layout',
      canvas: { aspectRatio: '16:9', backgroundColor: 'rgba(0,0,0,0.3)' },
      elements: [
        {
          id: 'dpad', type: 'dpad', position: { x: 15, y: 50 },
          zIndex: 1, size: 15, style: { color: '#333' },
        },
        {
          id: 'btn-a', type: 'button', position: { x: 85, y: 55 },
          zIndex: 1, shape: 'circle', size: 10, label: 'A', actionKey: 'A',
          style: { color: '#4CAF50' },
        },
      ],
    },
    container: document.getElementById('controller'),
    onInput: (e) => console.log(e.type, e.payload),
  });
</script>

All Exports

import { createControllerSDK } from '@ksteinstudio/game-controller';

import type {
  ControllerConfig,
  ButtonElement,
  JoystickElement,
  DpadElement,
  InputEvent,
  Position,
} from '@ksteinstudio/game-controller';

import {
  snapToGrid,
  snapPositionToGrid,
  pixelToPercentage,
  percentageToPixel,
  findAlignmentGuides,
  calculateCanvasDimensions,
} from '@ksteinstudio/game-controller';

import { createParentBridge, createIframeBridge } from '@ksteinstudio/game-controller';

import { renderControllerFromConfig, destroyRenderer } from '@ksteinstudio/game-controller';

License

MIT © Ksteinstudio