'bout that time to publicise this
This commit is contained in:
commit
60e7eb9618
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.map
|
31
README.md
Normal file
31
README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Spicetools DANCERUSH Pad Interface
|
||||||
|
![demo](img/demo.png)
|
||||||
|
|
||||||
|
|
||||||
|
This little blob of javascript communicates with the websocket API of Spicetools
|
||||||
|
to both display the pad LEDs of DANCERUSH, as well as control the foot inputs
|
||||||
|
(with multitouch support). It is best used on a separate touchscreen laptop or
|
||||||
|
phone, to maximise your finger-dancing vibes.
|
||||||
|
|
||||||
|
A great deal of effort has gone into making the LEDs look realistic, mainly
|
||||||
|
regarding colours saturating to white.
|
||||||
|
|
||||||
|
It uses PixiJS for rendering, and a standard WebSocket for comms.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
When you open the .html (no webserver needed), you'll see the default light
|
||||||
|
pattern:
|
||||||
|
![default pad state](img/default.png)
|
||||||
|
|
||||||
|
Setup spicetools to expose the API on a specific port. I used port 9001 (which
|
||||||
|
will expose the "real" API on 9001, and the websocket API on 9002). This can be done via the commandline with the `-api 9001` flag, or via spicecfg.exe:
|
||||||
|
![spicecfg](img/spicecfg.png)
|
||||||
|
|
||||||
|
Run the game, and the virtual pad should turn black as the connection is
|
||||||
|
established. From here, it should "just work". Click or touch the virtual pad to
|
||||||
|
send inputs to the game.
|
||||||
|
|
||||||
|
## Known bugs
|
||||||
|
Sometimes feet get stuck, which is much more common on devices with limited CPU
|
||||||
|
(such as phones). I never really investigated good ways to fix this. PRs
|
||||||
|
welcome.
|
BIN
img/default.png
Normal file
BIN
img/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 KiB |
BIN
img/demo.png
Normal file
BIN
img/demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
BIN
img/spicecfg.png
Normal file
BIN
img/spicecfg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
19
neopixel.html
Normal file
19
neopixel.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Neopixels</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="text/javascript" src="pixi.js"></script>
|
||||||
|
<script type="text/javascript" src="pixi-filters.js"></script>
|
||||||
|
<script type="text/javascript" src="viewport.js"></script>
|
||||||
|
<script type="text/javascript" src="neopixel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
350
neopixel.js
Normal file
350
neopixel.js
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
let circle_cache = {};
|
||||||
|
let blur_cache = {};
|
||||||
|
|
||||||
|
function light_set(light, r,g,b) {
|
||||||
|
light.glow.tint = b | g<<8 | r<<16;
|
||||||
|
|
||||||
|
// make bright lights become white
|
||||||
|
const clamp = 30;
|
||||||
|
mult = 1.5;
|
||||||
|
|
||||||
|
const rgb = [r,g,b];
|
||||||
|
const diff_arr = [0,0,0];
|
||||||
|
for(let i = 0; i < 3; i++) {
|
||||||
|
if(rgb[i] > clamp) {
|
||||||
|
const diff = rgb[i] - clamp;
|
||||||
|
|
||||||
|
for(let j = 0; j < 3; j++) {
|
||||||
|
diff_arr[j] += diff * mult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i = 0; i < 3; i++) {
|
||||||
|
rgb[i] += diff_arr[i];
|
||||||
|
if(rgb[i] > 255) {
|
||||||
|
rgb[i] = 255;
|
||||||
|
}
|
||||||
|
rgb[i] &= 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
r = rgb[0];
|
||||||
|
g = rgb[1];
|
||||||
|
b = rgb[2];
|
||||||
|
|
||||||
|
//light.led.tint = b | g<<8 | r<<16;
|
||||||
|
light.led.tint = b | g<<8 | r<<16;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_viewport(renderer, sceneContainer, gameWidth, gameHeight) {
|
||||||
|
const viewport = new Viewport.Viewport({
|
||||||
|
screenWidth: renderer.width,
|
||||||
|
screenHeight: renderer.height,
|
||||||
|
worldWidth: gameWidth,
|
||||||
|
worldHeight: gameHeight,
|
||||||
|
|
||||||
|
});
|
||||||
|
renderer.viewport = viewport;
|
||||||
|
scale_scene(renderer);
|
||||||
|
|
||||||
|
// add the viewport to the stage
|
||||||
|
sceneContainer.addChild(viewport);
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scale_scene(renderer) {
|
||||||
|
if(renderer.viewport) {
|
||||||
|
renderer.viewport.screenWidth = window.innerWidth;
|
||||||
|
renderer.viewport.screenHeight = window.innerHeight;
|
||||||
|
renderer.viewport.fitWorld();
|
||||||
|
renderer.viewport.moveCenter(renderer.viewport_width/2, renderer.viewport_height/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_circle(renderer, diameter) {
|
||||||
|
if(circle_cache[diameter]) {
|
||||||
|
return circle_cache[diameter];
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = new PIXI.Graphics();
|
||||||
|
p.beginFill(0xffffff);
|
||||||
|
p.drawCircle(diameter/2, diameter/2, diameter/2);
|
||||||
|
p.endFill();
|
||||||
|
|
||||||
|
const t = PIXI.RenderTexture.create(diameter, diameter);
|
||||||
|
renderer.render(p, t);
|
||||||
|
p.destroy();
|
||||||
|
|
||||||
|
circle_cache[diameter] = t;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blurred_circle(renderer, diameter, blur_diameter) {
|
||||||
|
const key = '' + diameter + '_' + blur_diameter;
|
||||||
|
if(blur_cache[key]) {
|
||||||
|
return blur_cache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const work_size = blur_diameter * 4;
|
||||||
|
|
||||||
|
const sprite = new PIXI.Sprite(render_circle(renderer, diameter));
|
||||||
|
sprite.anchor.set(0.5);
|
||||||
|
sprite.x = work_size / 2;
|
||||||
|
sprite.y = work_size / 2;
|
||||||
|
|
||||||
|
const blur = new PIXI.filters.BlurFilter();
|
||||||
|
blur.blur = blur_diameter;
|
||||||
|
blur.quality = 7;
|
||||||
|
|
||||||
|
const adjust = new PIXI.filters.AdjustmentFilter();
|
||||||
|
adjust.brightness = 10;
|
||||||
|
|
||||||
|
sprite.filters = [
|
||||||
|
blur,
|
||||||
|
adjust
|
||||||
|
];
|
||||||
|
|
||||||
|
const t = PIXI.RenderTexture.create(work_size, work_size);
|
||||||
|
renderer.render(sprite, t);
|
||||||
|
sprite.destroy();
|
||||||
|
|
||||||
|
blur_cache[key] = t;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_pixel(renderer, diameter) {
|
||||||
|
const sprite = new PIXI.Sprite(render_circle(renderer, diameter));
|
||||||
|
const glow = new PIXI.Sprite(render_blurred_circle(renderer, diameter, diameter*2.3));
|
||||||
|
// center the sprite's anchor point
|
||||||
|
sprite.anchor.set(0.5);
|
||||||
|
glow.anchor.set(0.5);
|
||||||
|
// makes it render bloom nicer for some reason
|
||||||
|
//sprite.rotation = 1;
|
||||||
|
sprite.tint = 0x000000;
|
||||||
|
glow.tint = 0x000000;
|
||||||
|
|
||||||
|
// sprite.filters = [
|
||||||
|
// new PIXI.filters.AdvancedBloomFilter({
|
||||||
|
// bloomScale : 2,
|
||||||
|
// brightness : 1.5,
|
||||||
|
// threshold : 0
|
||||||
|
// })
|
||||||
|
// ];
|
||||||
|
|
||||||
|
return {
|
||||||
|
'led': sprite,
|
||||||
|
'glow': glow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_array(app, x_count, y_count, x_gap, y_gap, led_diameter) {
|
||||||
|
const total_width = (x_count+1) * x_gap;
|
||||||
|
const total_height = (y_count+1) * y_gap;
|
||||||
|
app.renderer.viewport_width = total_width;
|
||||||
|
app.renderer.viewport_height = total_height;
|
||||||
|
world_width = total_width;
|
||||||
|
world_height = total_height;
|
||||||
|
|
||||||
|
viewport = create_viewport(app.renderer, app.stage, total_width, total_height);
|
||||||
|
viewport.pivot.x = -x_gap;
|
||||||
|
viewport.pivot.y = -y_gap;
|
||||||
|
|
||||||
|
viewport.interactive = true;
|
||||||
|
viewport.on("touchstart", handleStart, false);
|
||||||
|
viewport.on("touchendoutside", handleEnd, false);
|
||||||
|
viewport.on("touchend", handleEnd, false);
|
||||||
|
viewport.on("touchcancel", handleEnd, false);
|
||||||
|
viewport.on("touchmove", handleMove, false);
|
||||||
|
|
||||||
|
viewport.on("mousedown", mouseDown, false);
|
||||||
|
viewport.on("mouseup", mouseUp, false);
|
||||||
|
viewport.on("mouseupoutside", mouseUp, false);
|
||||||
|
viewport.on("mousemove", mouseMove, false);
|
||||||
|
|
||||||
|
// viewport.filters = [
|
||||||
|
// new PIXI.filters.AdvancedBloomFilter({
|
||||||
|
// bloomScale : 50,
|
||||||
|
// brightness : 15,
|
||||||
|
// threshold : 0,
|
||||||
|
// quality : 6
|
||||||
|
// })
|
||||||
|
// ];
|
||||||
|
|
||||||
|
let leds = [];
|
||||||
|
for(let y = 0; y < y_count; y++) {
|
||||||
|
for(let x = 0; x < x_count; x++) {
|
||||||
|
|
||||||
|
const light = create_pixel(app.renderer, led_diameter);
|
||||||
|
// move the sprite to the center of the screen
|
||||||
|
light.led.x = x * x_gap;
|
||||||
|
light.glow.x = x * x_gap;
|
||||||
|
light.led.y = y * y_gap;
|
||||||
|
light.glow.y = y * y_gap;
|
||||||
|
leds.push(light);
|
||||||
|
|
||||||
|
let diff = 3;
|
||||||
|
light_set(light, 0xff,0,0);
|
||||||
|
if(x == 0 || x == x_count-1 || y == 0 || y == y_count-1) {
|
||||||
|
light_set(light, 0,0xff,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add in sequence to batch sprite types
|
||||||
|
for(let i = 0; i < leds.length; i++) {
|
||||||
|
viewport.addChild(leds[i].led);
|
||||||
|
}
|
||||||
|
for(let i = 0; i < leds.length; i++) {
|
||||||
|
viewport.addChild(leds[i].glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return leds;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new PIXI.Application({
|
||||||
|
backgroundColor: 0,
|
||||||
|
antialias: true,
|
||||||
|
//resizeTo: document.body
|
||||||
|
// width: window.innerWidth,
|
||||||
|
// height: window.innerHeight
|
||||||
|
});
|
||||||
|
document.body.appendChild(app.view);
|
||||||
|
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
|
||||||
|
// Resize function window
|
||||||
|
function resize() {
|
||||||
|
// Resize the renderer
|
||||||
|
app.renderer.resize(window.innerWidth, window.innerHeight);
|
||||||
|
scale_scene(app.renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize();
|
||||||
|
|
||||||
|
let world_width = 0;
|
||||||
|
let world_height = 0;
|
||||||
|
const leds = create_array(app, 38, 49, 1/30*1000, 1/30*1000, 10);
|
||||||
|
let connected = false;
|
||||||
|
//console.log(leds);
|
||||||
|
|
||||||
|
app.ticker.add((delta) => {
|
||||||
|
if(connected && socket.bufferedAmount < 256) {
|
||||||
|
const payload = json_to_bin({
|
||||||
|
"id":0,
|
||||||
|
"module":"drs",
|
||||||
|
"function":"tapeled_get",
|
||||||
|
"params":[],
|
||||||
|
});
|
||||||
|
socket.send(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function send_touch(type, evt) {
|
||||||
|
if(!connected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const world = app.renderer.viewport.toWorld(evt.data.global.x, evt.data.global.y);
|
||||||
|
const x = world.x/world_width + 1/38;
|
||||||
|
const y = world.y/world_height + 1/49;
|
||||||
|
const width = 0.15;
|
||||||
|
const height = 0.15;
|
||||||
|
|
||||||
|
const payload = json_to_bin({
|
||||||
|
"id":0,
|
||||||
|
"module":"drs",
|
||||||
|
"function":"touch_set",
|
||||||
|
"params":[
|
||||||
|
//[type,evt.data.identifier,evt.data.global.x,evt.data.global.y,1696,1312]
|
||||||
|
[type,evt.data.identifier,x,y,width,height]
|
||||||
|
],
|
||||||
|
});
|
||||||
|
socket.send(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStart(evt) {
|
||||||
|
// console.log(evt);
|
||||||
|
// console.log("touchstart:", evt.data.identifier, evt.data.global.x, evt.data.global.y);
|
||||||
|
send_touch(0, evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMove(evt) {
|
||||||
|
// console.log(evt);
|
||||||
|
// console.log("touchmove:", evt.data.identifier, evt.data.global.x, evt.data.global.y);
|
||||||
|
send_touch(2, evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnd(evt) {
|
||||||
|
// console.log(evt);
|
||||||
|
// console.log("touchend:", evt.data.identifier, evt.data.global.x, evt.data.global.y);
|
||||||
|
send_touch(1, evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouseIsMoving = false;
|
||||||
|
|
||||||
|
function mouseDown(evt) {
|
||||||
|
mouseIsMoving = true;
|
||||||
|
handleStart(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseMove(evt) {
|
||||||
|
if(mouseIsMoving) {
|
||||||
|
handleMove(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseUp(evt) {
|
||||||
|
mouseIsMoving = false;
|
||||||
|
handleEnd(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function json_to_bin(json_obj) {
|
||||||
|
const json_encoded = JSON.stringify(json_obj);
|
||||||
|
return new TextEncoder().encode(json_encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bin_to_json(bin) {
|
||||||
|
const decoded = new TextDecoder().decode(bin);
|
||||||
|
return JSON.parse(decoded.replace('\0', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket = undefined;
|
||||||
|
function new_websocket() {
|
||||||
|
socket = new WebSocket("ws://localhost:9002");
|
||||||
|
socket.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
socket.onopen = function(e) {
|
||||||
|
console.log("[open] Connection established");
|
||||||
|
connected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = function(event) {
|
||||||
|
// console.log(`[message] Data received from server: ${event.data}`);
|
||||||
|
// console.log(`[message] Data received from server:`, bin_to_json(event.data));
|
||||||
|
const packet = bin_to_json(event.data);
|
||||||
|
if(packet.error) {
|
||||||
|
console.log("Bad response", packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = packet.data[0];
|
||||||
|
|
||||||
|
if(!data)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for(let i = 0; i < 38*49; i++) {
|
||||||
|
let r = data[i*3] & 0xfe;
|
||||||
|
let g = data[i*3+1] & 0xfe;
|
||||||
|
let b = data[i*3+2] & 0xfe;
|
||||||
|
|
||||||
|
light_set(leds[i], r,g,b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function(event) {
|
||||||
|
connected = false;
|
||||||
|
setTimeout(() => {new_websocket()}, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
new_websocket();
|
9
pixi-filters.js
Normal file
9
pixi-filters.js
Normal file
File diff suppressed because one or more lines are too long
2
viewport.js
Normal file
2
viewport.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user