npm package: @ksteinstudio/game-controller
Description: Universal Game Controller Engine - render interactive UI layouts via JSON configuration in an Iframe-based SDK. Percentage-based positioning, multi-touch, zero dependencies.
Author: Ibrahim Karl Bileri
Latest version: 1.0.1

# Universal Game Controller Engine

[![npm version](https://img.shields.io/npm/v/@ksteinstudio/game-controller)](https://www.npmjs.com/package/@ksteinstudio/game-controller)
[![license](https://img.shields.io/npm/l/@ksteinstudio/game-controller)](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

```bash
# 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)

```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

```typescript
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.

```typescript
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

```typescript
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

```typescript
import { pixelToPercentage, percentageToPixel } from '@ksteinstudio/game-controller';

const pct = pixelToPercentage(320, 1920); // → 16.67
const px = percentageToPixel(50, 1080);   // → 540
```

### Alignment Guides

```typescript
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

```tsx
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

```vue
<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)

```html
<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

```typescript
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](LICENSE) © Ksteinstudio