Skip to content

Commit

Permalink
Support rendering button as images (#2)
Browse files Browse the repository at this point in the history
* Show config name in the displayed info

* Change createDiv into createElem

* Add image rendering from the config

* Support button widget as images

* Add support for the Mega Drive 6 button controller

* Update documentation
  • Loading branch information
danikaze authored Aug 16, 2020
1 parent 0aeab8a commit bbde973
Show file tree
Hide file tree
Showing 19 changed files with 537 additions and 73 deletions.
50 changes: 43 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Generate a browser source (to use via [OBS](https://obsproject.com/) or any other tool) displaying an overlay of your HOTAS controller in real time.

![Preview screenshot][screenshot]
![Preview screenshot][elite-screenshot]

![Preview screenshot][megadrive-screenshot]

## How to use it

Expand All @@ -14,7 +16,28 @@ In `OBS` add a `New source > Browser` and enter the provided `OBS URL` in the bo

First version is just a PoC (_Proof of Concept_) where the shown controls are what **I** find interesting to show for **Elite Dangerous**, and the bindings are the ones I use with my **X-56**, so every value is harcoded.

However, bindings are configurable by changing the [config file](src/elite.ts) and rebuilding the project.
However, bindings are configurable by changing the [Elite Dangerous](src/elite.ts) and [Mega Drive](src/megadrive6.ts) config files for and rebuilding the project.

The html will show the Elite one by default, but adding `?config=megadrive` should show the other one.

## Support

This project plans to support all kind of inputs. Those (and their progress) are:

- ☑ Digital buttons with CSS
- ☑ Digital buttons with images
- ☑ 1-axis with CSS
- ☐ 1-Analog axis with images
- ☑ 2-axis with CSS
- ☐ 2-Analog axis with images
- ☑ Digital buttons shown as 1-axis with CSS
- ☑ Digital buttons shown as 2-axis with CSS
- ☐ Digital buttons shown as 1-axis with images
- ☐ Digital buttons shown as 2-axis with images
- ☑ 1-axis shown as a digital button with CSS
- ☑ 1-axis shown as a digital button with images
- ☐ 1-axis shown as an analog button with CSS
- ☐ 1-axis shown as an analog button with images

## Rebuilding

Expand All @@ -32,16 +55,29 @@ npm build

Building will generate the required files in the `app` folder, the `index.html` file is the one to use.

[screenshot]: ./img/screenshot-0.1.0.png 'HOTAS overlay preview'

## Changelog

### 0.2.0

Allow to fully customize the layout from a [config file](src/elite.ts).

Config file is still hardcoded tho, but it's a big step.
- Allow to fully customize the layout from a [config file](src/elite.ts).
Config file is still hardcoded tho, but it's a big step.
- Add configuration file for the Mega Drive mini 6 button controller.
- Add support for:
- Digital buttons with images
- 1-axis shown as a digital button with CSS
- 1-axis shown as a digital button with images

### 0.1.0

First version. PoC to test the creation of a browser source in OBS.

It supports:

- Digital buttons with CSS
- 1-axis with CSS
- 2-axis with CSS
- Digital buttons shown as 1-axis with CSS
- Digital buttons shown as 2-axis with CSS

[elite-screenshot]: ./img/elite-0.2.0.gif 'HOTAS overlay preview'
[megadrive-screenshot]: ./img/megadrive-0.2.0.gif 'Mega Drive overlay preview'
Binary file added img/elite-0.2.0.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/megadrive-0.2.0.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 14 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Config } from './constants';
import { renderWidget } from './widgets';
import { renderImage, appendChildren } from './dom';

export function renderConfig(config: Config): void {
const ROOT_ID = 'container';
Expand All @@ -10,10 +11,17 @@ export function renderConfig(config: Config): void {
parent.id = ROOT_ID;
}

config.widgets.forEach((def) => {
const widget = renderWidget(def);
if (widget) {
parent.appendChild(widget);
}
});
config.images &&
config.images.forEach((def) => {
const img = renderImage(def);
if (img) {
parent.appendChild(img);
}
});

config.widgets &&
config.widgets.forEach((def) => {
const widget = renderWidget(def);
appendChildren(parent, widget);
});
}
37 changes: 32 additions & 5 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,26 @@ export type PadMapping = { [pad: string]: string | RegExp };

export interface Config {
version: number;
name: string;
images?: Image[];
widgets: Widget[];
}

export interface Image {
src: string;
notes?: string;
// position of the image relative to the parent
top?: number | string;
left?: number | string;
// size of the displayed image (or its cropped area)
width?: number | string;
height?: number | string;
// if cropped, start point (default to 0,0)
offsetX?: number;
offsetY?: number;
zIndex?: number;
}

export type Widget = WidgetGroup | WidgetButton | WidgetAxis;

export interface WidgetLabel {
Expand Down Expand Up @@ -37,25 +54,35 @@ export interface InputAxis {
inverted?: boolean;
}

export interface InputAxisButton {
type: 'axis-button';
export interface InputButtonAsAxis {
type: 'button-as-axis';
min: InputButton;
max: InputButton;
inverted?: boolean;
}

export interface InputAxisAsButton {
type: 'axis-as-button';
pad: string;
axis: number;
// axis value must be inside [min, max] to be considered as pressed
min: number;
max: number;
}

export interface WidgetGroup extends WidgetBase<'group'> {
children: Widget[];
}

export interface WidgetButton extends WidgetBase<'button'> {
input: InputButton;
input: InputButton | InputAxisAsButton;
images?: Image | Image[];
}

export interface WidgetAxis extends WidgetBase<'axis', 'position'> {
gridlines?: number[];
input: {
x?: InputAxis | InputAxisButton;
y?: InputAxis | InputAxisButton;
x?: InputAxis | InputButtonAsAxis;
y?: InputAxis | InputButtonAsAxis;
};
}
80 changes: 73 additions & 7 deletions src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import { WidgetLabel } from './constants';
import { WidgetLabel, Image } from './constants';

export interface DivOptions {
export interface DivOptions<T extends keyof HTMLElementTagNameMap = 'div'> {
id?: string;
type?: T;
classes?: string | string[] | undefined;
styles?: { [key: string]: string };
parent?: HTMLElement;
children?: HTMLElement[];
text?: string;
data?: { [key: string]: string | number };
attr?: { [key: string]: string | number };
}

export function createDiv(options: DivOptions = {}): HTMLDivElement {
const div = document.createElement('div');
export function appendChildren(
parent: HTMLElement,
children?: HTMLElement | HTMLElement[]
): void {
if (!children) return;

(Array.isArray(children) ? children : [children]).forEach((child) => {
parent.appendChild(child);
});
}

export function createElem<T extends keyof HTMLElementTagNameMap = 'div'>(
options: DivOptions<T> = {}
): HTMLElementTagNameMap[T] {
const div = document.createElement(
options.type || ('div' as keyof HTMLElementTagNameMap)
);

if (options.id) {
div.id = options.id;
Expand All @@ -26,14 +44,18 @@ export function createDiv(options: DivOptions = {}): HTMLDivElement {

if (options.styles) {
Object.entries(options.styles).forEach(([key, value]) => {
div.style.setProperty(key, value);
// using style.setProperty doesn't work for all values :(
// tslint:disable: no-any
div.style[key as any] = value;
});
}

if (options.parent) {
options.parent.appendChild(div);
}

appendChildren(div, options.children);

if (options.text) {
div.innerText = options.text;
}
Expand All @@ -44,17 +66,61 @@ export function createDiv(options: DivOptions = {}): HTMLDivElement {
});
}

return div;
if (options.attr) {
Object.entries(options.attr).forEach(([key, value]) => {
div.setAttribute(key, String(value));
});
}

return div as HTMLElementTagNameMap[T];
}

export function addLabels(elem: HTMLElement, labels?: WidgetLabel): void {
if (!labels) return;

Object.entries(labels).forEach(([key, text]) => {
createDiv({
createElem({
text,
classes: `label label-${key}`,
parent: elem,
});
});
}

export function renderImage(image: Image): HTMLDivElement {
const container = createElem({
classes: 'img-container',
styles: {
top: getPx(image.top || 0),
left: getPx(image.left || 0),
width: getPx(image.width),
height: getPx(image.height),
zIndex: String(image.zIndex || 0),
},
});

if (image.width || image.height || image.offsetX || image.offsetY) {
container.style.overflow = 'hidden';
}

const img = createElem({
type: 'img',
parent: container,
attr: {
src: image.src,
},
styles: {
top: getPx(-1 * image.offsetY!),
left: getPx(-1 * image.offsetX!),
},
});
img.src = image.src;

return container;
}

function getPx(value?: number | string): string {
if (isNaN(value as number)) return '';
if (typeof value === 'number') return `${value}px`;
return value!;
}
3 changes: 2 additions & 1 deletion src/elite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Config } from './constants';

export const config: Config = {
version: 1,
name: 'Logitech X-56 for Elite Dangerous',
widgets: [
{
type: 'group',
Expand Down Expand Up @@ -213,7 +214,7 @@ export const config: Config = {
gridlines: [50],
input: {
y: {
type: 'axis-button',
type: 'button-as-axis',
min: {
type: 'button',
pad:
Expand Down
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { initControllers } from './controllers';
import { updateInfo } from './info';
import { renderConfig } from './config';
import { config } from './elite';
import { config as megaDrive6config } from './megadrive6';
import { config as eliteConfig } from './elite';

const url = new URL(location.href);
if (url.searchParams.get('display')) {
document.body.classList.add('display');
}
const configGetParam = url.searchParams.get('config');
const config = /mega/.test(configGetParam!) ? megaDrive6config : eliteConfig;

renderConfig(config);
initControllers();
updateInfo();
updateInfo(config);
32 changes: 20 additions & 12 deletions src/info.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
export function updateInfo() {
import { Config } from './constants';

export function updateInfo(config: Config) {
updateConfig(config);
updateSize();
updateUrl();
updateUa();
}

function updateConfig(config: Config) {
setInfoElement('config-name', config.name);
}

function updateSize() {
const heightFixPx = 3;
const elem = document.getElementById('container-size');
if (!elem) return;

let w = 0;
let h = 0;
Expand All @@ -18,20 +23,23 @@ function updateSize() {
h = Math.max(h, bounds.bottom);
});

elem.innerText = `${w} x ${h + heightFixPx}`;
setInfoElement('container-size', `${w} x ${h + heightFixPx}`);
}

function updateUrl() {
const elem = document.getElementById('obs-url');
if (!elem) return;

const url = `${location.protocol}//${location.pathname}?display=1`;
elem.innerText = url;
const url = new URL(location.href);
url.searchParams.set('display', '1');
setInfoElement('obs-url', url.href);
}

function updateUa() {
const elem = document.getElementById('ua-info');
if (!elem) return;
setInfoElement('ua-info', navigator.userAgent);
}

elem.innerText = navigator.userAgent;
function setInfoElement(classname: string, info: string = '') {
document
.querySelectorAll<HTMLSpanElement>(`#info .${classname} span`)
.forEach((elem) => {
elem.innerText = info;
});
}
Loading

0 comments on commit bbde973

Please sign in to comment.