-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
1,983 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,13 @@ | ||
# data-universe | ||
|
||
3D visualization of job market data with VR controls. | ||
|
||
## Setup | ||
|
||
`npm install` will install dependencies. | ||
|
||
`npm start` will run the project using webpack-development-server. | ||
|
||
`node controls-server.js` runs the Xbox control server, the webpack instance must be running before this starts. | ||
|
||
Connect your phone with Safari or Chrome to port 8080 (if your firewall does not allow this setup a Wifi hotspot with another phone). |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
const XboxController = require('xbox-controller'); | ||
const WebSocket = require('ws'); | ||
|
||
const xbox = new XboxController(); | ||
const server = new WebSocket.Server({ port: 8081 }); | ||
|
||
server.on('connection', () => { | ||
/* eslint no-console: "allow" */ | ||
console.log('Client connected.'); | ||
}); | ||
|
||
server.on('connection', (socket) => { | ||
socket.on('message', (packet) => { | ||
const message = JSON.parse(packet); | ||
switch (message.type) { | ||
case 'selected': | ||
console.log(message); | ||
broadcast(message); | ||
break; | ||
} | ||
}); | ||
}); | ||
|
||
xbox.on('left:move', (position) => { | ||
const message = { | ||
type: 'left:move', | ||
x: position.x, | ||
y: position.y, | ||
}; | ||
broadcast(message); | ||
}); | ||
|
||
xbox.on('right:move', (position) => { | ||
const message = { | ||
type: 'right:move', | ||
x: position.x, | ||
y: position.y, | ||
}; | ||
broadcast(message); | ||
}); | ||
|
||
xbox.on('lefttrigger', (position) => { | ||
const message = { | ||
type: 'lefttrigger', | ||
x: position, | ||
}; | ||
broadcast(message); | ||
}); | ||
|
||
xbox.on('righttrigger', (position) => { | ||
const message = { | ||
type: 'righttrigger', | ||
x: position, | ||
}; | ||
broadcast(message); | ||
}); | ||
|
||
xbox.on('a:release', (key) => { | ||
const message = { | ||
type: 'a:release', | ||
}; | ||
broadcast(message); | ||
}); | ||
|
||
xbox.on('start:release', (key) => { | ||
const message = { | ||
type: 'start:release', | ||
}; | ||
broadcast(message); | ||
}); | ||
|
||
function broadcast(message) { | ||
const packet = JSON.stringify(message); | ||
server.clients.forEach((client) => { | ||
if (client.readyState === WebSocket.OPEN) { | ||
client.send(packet); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
module.exports = function threeLoader(content) { | ||
if (this.cacheable) this.cacheable(); | ||
return ` | ||
import { Euler } from 'three/math/Euler'; | ||
import { Vector3 } from 'three/math/Vector3'; | ||
import { Quaternion } from 'three/math/Quaternion'; | ||
import { _Math } from 'three/math/Math'; | ||
const THREE = { | ||
Euler, | ||
Vector3, | ||
Quaternion, | ||
Math: _Math | ||
}; | ||
${content} | ||
const DeviceOrientationControls = THREE.DeviceOrientationControls; | ||
export { DeviceOrientationControls }; | ||
`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module.exports = function threeLoader(content) { | ||
if (this.cacheable) this.cacheable(); | ||
return ` | ||
import { StereoCamera } from 'three/cameras/StereoCamera'; | ||
const THREE = { | ||
StereoCamera, | ||
}; | ||
${content} | ||
const StereoEffect = THREE.StereoEffect; | ||
export { StereoEffect }; | ||
`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { Vector2 } from 'three/math/Vector2'; | ||
import { Vector3 } from 'three/math/Vector3'; | ||
import { Quaternion } from 'three/math/Quaternion'; | ||
|
||
const joystickMagnitude = 32767.0; | ||
const triggerMagnitude = 255.0; | ||
|
||
export function XboxRemoteControls(object) { | ||
this.object = object; | ||
|
||
this.movementSpeed = 10.0; | ||
this.rotationSpeed = 0.5; | ||
|
||
this.movement = new Vector3(0, 0, 0); | ||
this.rotation = new Vector2(0, 0); | ||
|
||
this.rotationQuaternion = new Quaternion(); | ||
|
||
this.controls = { | ||
movement: { x: 0, y: 0 }, | ||
rotation: { x: 0, y: 0 }, | ||
elevate: { up: 0, down: 0 }, | ||
}; | ||
|
||
this.onMessage = (message) => { | ||
switch (message.type) { | ||
case 'left:move': | ||
this.controls.movement.x = message.x; | ||
this.controls.movement.y = message.y; | ||
break; | ||
case 'right:move': | ||
this.controls.rotation.x = message.x; | ||
this.controls.rotation.y = message.y; | ||
break; | ||
case 'righttrigger': | ||
this.controls.elevate.up = message.x; | ||
break; | ||
case 'lefttrigger': | ||
this.controls.elevate.down = message.x; | ||
break; | ||
default: | ||
break; | ||
} | ||
}; | ||
|
||
this.update = (delta) => { | ||
const movementMultiplier = (delta * this.movementSpeed); | ||
this.movement.x = this.controls.movement.x / joystickMagnitude; | ||
this.movement.y = (this.controls.elevate.up - this.controls.elevate.down) / triggerMagnitude; | ||
this.movement.z = this.controls.movement.y / joystickMagnitude; | ||
this.movement.multiplyScalar(movementMultiplier); | ||
|
||
this.object.translateX(this.movement.x); | ||
this.object.translateY(this.movement.y); | ||
this.object.translateZ(this.movement.z); | ||
|
||
// const rotationMultiplier = (delta * this.rotationSpeed) / joystickMagnitude; | ||
// this.rotation.set(this.controls.rotation.x, this.controls.rotation.y); | ||
// this.rotation.multiplyScalar(rotationMultiplier); | ||
// this.rotationQuaternion.set(-this.rotation.y, -this.rotation.x, 0, 1); | ||
// this.object.quaternion.multiply(this.rotationQuaternion); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { Object3D } from 'three/core/Object3D'; | ||
import { Texture } from 'three/textures/Texture'; | ||
import { SpriteMaterial } from 'three/materials/SpriteMaterial'; | ||
import { Sprite } from 'three/objects/Sprite'; | ||
|
||
export function createBillboard(text, subtext, height) { | ||
const billboard = new Object3D(); | ||
const sprite = createSprite(text, subtext); | ||
const y = height + 0.5; | ||
sprite.position.set(0, y, 0); | ||
billboard.add(sprite); | ||
billboard.isBillboard = true; | ||
return billboard; | ||
} | ||
|
||
export function updateBillboard(billboard, camera) { | ||
billboard.quaternion.copy(camera.quaternion); | ||
} | ||
|
||
function createSprite(text, subtext) { | ||
const texture = createTexture(text, subtext); | ||
texture.needsUpdate = true; | ||
|
||
const material = new SpriteMaterial({ map: texture, fog: true }); | ||
const sprite = new Sprite(material); | ||
return sprite; | ||
} | ||
|
||
function createTexture(text, subtext) { | ||
// These constants can be tweaked to change the styling. | ||
const fontFace = 'Arial'; | ||
const headerFontSize = 24; | ||
const infoFontSize = 20; | ||
const backgroundColor = 'rgba(50,75,75,0.5)'; | ||
const headerTextColor = 'rgba(255,255,255,0.75)'; | ||
const infoTextColor = 'rgba(255,255,255,0.60)'; | ||
const padding = 10; | ||
const spacing = 18; | ||
const boxWidth = 256; // Must be a power of 2. | ||
// Removes some height from the bottom of the box to make the text look more centered. | ||
const heightReduction = 0.1 * headerFontSize; | ||
|
||
//Calculate the height of a line | ||
const headerLineHeight = 1.4 * headerFontSize; | ||
const infoLineHeight = 1.4 * infoFontSize; | ||
|
||
// Setup Canvas | ||
const canvas = document.createElement('canvas'); | ||
canvas.width = boxWidth; | ||
canvas.height = boxWidth; | ||
const context = canvas.getContext('2d'); | ||
|
||
// Calculate properties and wrap text | ||
const contentWidth = boxWidth - (2 * padding); | ||
context.font = `Normal ${headerFontSize}px ${fontFace}`; | ||
const headerLines = wrapText(context, text, contentWidth); | ||
context.font = `Normal ${infoFontSize}px ${fontFace}`; | ||
const infoLines = wrapText(context, subtext, contentWidth); | ||
|
||
// Calculate the heights of each segment | ||
const headerTotalLineHeight = (headerLines.length * headerLineHeight); | ||
const infoHeight = (infoLines.length * infoLineHeight); | ||
const totalLineHeight = headerTotalLineHeight + infoHeight; | ||
|
||
// Calculate the start of info text segment | ||
const infoTextStart = spacing + (headerTotalLineHeight + padding); | ||
|
||
const contentHeight = (totalLineHeight + spacing) - heightReduction; | ||
const boxHeight = contentHeight + (2 * padding); | ||
const yOffset = canvas.height - boxHeight; | ||
|
||
// Render box. | ||
context.fillStyle = backgroundColor; | ||
context.fillRect(0, yOffset, boxWidth, boxHeight); | ||
|
||
// Render header text. | ||
context.textBaseline = 'top'; | ||
context.font = `Normal ${headerFontSize}px ${fontFace}`; | ||
context.fillStyle = headerTextColor; | ||
|
||
headerLines.forEach((line, i) => { | ||
const lineWidth = context.measureText(line).width; | ||
const lineX = padding + ((contentWidth - lineWidth) / 2); | ||
const lineY = padding + (i * headerLineHeight) + yOffset; | ||
context.fillText(line, lineX, lineY, contentWidth); | ||
}); | ||
|
||
// Draws line divider | ||
context.beginPath(); | ||
context.moveTo(0, ((headerTotalLineHeight + padding) + yOffset) + (spacing / 2)); | ||
context.lineTo(boxWidth, ((headerTotalLineHeight + padding) + yOffset) + (spacing / 2)); | ||
context.strokeStyle = 'rgba(50,75,75,0.8)'; | ||
context.stroke(); | ||
|
||
// Render info text | ||
context.font = `Normal ${infoFontSize}px ${fontFace}`; | ||
context.fillStyle = infoTextColor; | ||
|
||
infoLines.forEach((line, i) => { | ||
const lineWidth = context.measureText(line).width; | ||
const lineX = padding + ((contentWidth - lineWidth) / 2); | ||
const lineY = infoTextStart + (i * infoLineHeight) + yOffset; | ||
context.fillText(line, lineX, lineY, contentWidth); | ||
}); | ||
|
||
return new Texture(canvas); | ||
} | ||
|
||
function wrapText(context, text, maxWidth) { | ||
const words = text.split(' '); | ||
if (words.length <= 1) { | ||
return [text]; | ||
} | ||
|
||
const lines = []; | ||
let line = words[0]; | ||
|
||
for (let i = 1; i < words.length; i += 1) { | ||
const word = words[i]; | ||
const testLine = `${line} ${word}`; | ||
const testWidth = context.measureText(testLine).width; | ||
if (testWidth >= maxWidth) { | ||
lines.push(line); | ||
line = word; | ||
} | ||
else { | ||
line = testLine; | ||
} | ||
} | ||
lines.push(line); | ||
|
||
return lines; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,12 @@ | ||
import { PerspectiveCamera } from 'three/cameras/PerspectiveCamera'; | ||
|
||
function createCamera() { | ||
export function createCamera() { | ||
const camera = new PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.1, 1e7); | ||
camera.position.z = 5; | ||
return camera; | ||
} | ||
|
||
function resizeCamera(camera) { | ||
export function resizeCamera(camera) { | ||
camera.aspect = window.innerWidth / window.innerHeight; | ||
camera.updateProjectionMatrix(); | ||
} | ||
|
||
export { createCamera, resizeCamera }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,20 @@ | ||
import { FlyControls } from 'three_examples/controls/FlyControls'; | ||
import { DeviceOrientationControls } from 'three_examples/controls/DeviceOrientationControls'; | ||
|
||
function createControls(camera, container) { | ||
export function createFlyControls(camera, container) { | ||
const controls = new FlyControls(camera, container); | ||
|
||
controls.movementSpeed = 2.0; | ||
controls.movementSpeed = 10.0; | ||
controls.rollSpeed = Math.PI / 24; | ||
controls.autoForward = false; | ||
controls.dragToLook = false; | ||
|
||
return controls; | ||
} | ||
|
||
export { createControls }; | ||
export function createVRControls(camera, container) { | ||
const controls = new DeviceOrientationControls(camera, container); | ||
controls.connect(); | ||
|
||
return controls; | ||
} |
Oops, something went wrong.