@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.
Maintainers
Readme
Universal Game Controller Engine
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); // → 540Alignment Guides
import { findAlignmentGuides } from '@ksteinstudio/game-controller';
const result = findAlignmentGuides(draggedElement, cursorPosition, otherElements);
// result.position → snapped position
// result.guides → array of active alignment linesFramework 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
