an e-sports simulation targeting the HTC Vive
take the Vive controllers and tap one of the spheres. watch those beautiful collisions!
hat-tip to @donrmccurdy for packaging up the very nice aframe-physics-system
to enjoy this experience on the Vive, follow the instructions at https://webvr.info/ to download and install an experimental browser build that supports WebVR. from there, click the Enter VR
HMD icon on the bottom right corner of the browser window to enter the scene.
for reference, this experience was developed on the Aug 29 2016
version 55.0.2842.0
build of Chromium with the flags --enable-webvr
and --enable-gamepad-extensions
to explore the scene on a 2D screen, open in a new tab and then hold the S
key until the scatterplot and legend come into view. from there you can navigate using the W A S D
keys and look by clicking and dragging with the mouse
this block is a fork of the roomscale scatterplot block which in turn is an iteration on the #aframevr + #d3js Iris Graph from @bryik_ws
for more A-Frame + D3 experiments
search for aframe
on blockbuilder search
http://blockbuilder.org/search#text=aframe
<!DOCTYPE html>
<meta charset="utf-8">
<script src="aframe.min.js"></script>
<script src="aframe-extras.min.js"></script>
<script src="https://cdn.rawgit.com/donmccurdy/aframe-physics-system/v1.0.3/dist/aframe-physics-system.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.3.0/d3.min.js"></script>
<script src="https://rawgit.com/bryik/aframe-bmfont-text-component/master/dist/aframe-bmfont-text-component.min.js"></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js'></script>
<script src="aframe-scatter-component.js"></script>
<body>
<a-scene physics="debug: true; restitution: 0.5; friction: 0.005; gravity: 0" antialias='true'>
<!-- Camera -->
<a-entity camera look-controls wasd-controls></a-entity>
<!-- Graph -->
<a-entity position="0 0 -1"
graph="width: 2;
height: 2;
depth: 2">
</a-entity>
<!-- a box of planes -->
<a-entity>
<!-- front -->
<a-box
color="teal"
height="2"
width="2"
depth="0.01"
position="0 1 0"
rotation="0 0 0"
static-body
material="transparent:true; opacity:0.2">
</a-box>
<!-- back -->
<a-box
color="teal"
height="2"
width="2"
depth="0.01"
position="0 1 -2"
rotation="0 0 0"
static-body
material="transparent:true; opacity:0.2">
</a-box>
<!-- top -->
<a-box
color="teal"
height="2"
width="2"
depth="0.01"
position="0 2 -1"
rotation="90 0 0"
static-body
material="transparent:true; opacity:0.2">
</a-box>
<!-- bottom -->
<a-box
color="teal"
height="2"
width="2"
depth="0.01"
position="0 0 -1"
rotation="90 0 0"
static-body
material="transparent:true; opacity:0.2">
</a-box>
<!-- left -->
<a-box
color="teal"
height="2"
width="2"
depth="0.01"
position="1 1 -1"
rotation="0 90 0"
static-body
material="transparent:true; opacity:0.2">
</a-box>
<!-- right -->
<a-box
color="teal"
height="2"
width="2"
depth="0.01"
position="-1 1 -1"
rotation="0 90 0"
static-body
material="transparent:true; opacity:0.2">
</a-box>
</a-entity>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
<script>
function render () {
//
// after adding all objects to the scene
// add the Vive controllers
//
// add left controller
d3.select('a-scene')
.append('a-entity')
.attr('id', 'leftController')
.attr('vive-controls', 'hand: left')
.attr('static-body', 'shape: sphere; sphereRadius: 0.02;')
.attr('sphere-collider', 'objects: .throwable')
.attr('grab', '')
// add right controller
d3.select('a-scene')
.append('a-entity')
.attr('id', 'rightController')
.attr('vive-controls', 'hand: right')
.attr('static-body', 'shape: sphere; sphereRadius: 0.02;')
.attr('sphere-collider', 'objects: .throwable')
.attr('grab', '')
};
var sceneEl = document.querySelector('a-scene');
if (sceneEl.hasLoaded) {
render();
} else {
sceneEl.addEventListener('loaded', render);
}
</script>
</body>
AFRAME.registerComponent('graph', {
schema: {
csv: {
type: 'string'
},
id: {
type: 'int',
default: '0'
},
width: {
type: 'number',
default: 1
},
height: {
type: 'number',
default: 1
},
depth: {
type: 'number',
default: 1
}
},
/**
* Called once when component is attached. Generally for initial setup.
*/
update: function () {
// Entity data
var el = this.el;
var object3D = el.object3D;
var data = this.data;
var width = data.width;
var height = data.height;
var depth = data.depth;
// These will be used to set the range of the axes' scales
var xRange = [0 + 0.1, width - 0.1];
var yRange = [0 + 0.1, height - 0.1];
var zRange = [0 - 0.1, -depth + 0.1];
// var xRange = [0, width];
// var yRange = [0, height];
// var zRange = [0, -depth];
/**
* Create origin point.
* This gives a solid reference point for scaling data.
* It is positioned at the vertex of the left grid and bottom grid (towards the front).
*/
var originPointPosition = (-width / 2) + ' 0 ' + (depth / 2);
var originPointID = 'originPoint' + data.id;
d3.select(el).append('a-entity')
.attr('id', originPointID)
.attr('position', originPointPosition);
// Create graphing area out of three textured planes
// var grid = gridMaker(width, height, depth);
// object3D.add(grid);
// Label axes
// TODO: add a text measuring function
// then measure label text length
// the use that length to
// sprogrammatically position labels
// var xLabelPosition = '0.2' + ' ' + '-0.1' + ' ' + '0.1';
// var xLabelRotation = '-45' + ' ' + '0' + ' ' + '0';
// d3.select('#' + originPointID)
// .append('a-entity')
// .attr('id', 'x')
// .attr('bmfont-text', 'text: Sepal Length (cm)')
// .attr('position', xLabelPosition)
// .attr('rotation', xLabelRotation);
// var yLabelPosition = (width + 0.12) + ' ' + '0.2' + ' ' + (-depth + 0.08);
// var yLabelRotation = '0' + ' ' + '-30' + ' ' + '90';
// d3.select('#' + originPointID)
// .append('a-entity')
// .attr('id', 'y')
// .attr('bmfont-text', 'text: Petal Length (cm)')
// .attr('position', yLabelPosition)
// .attr('rotation', yLabelRotation);
// var zLabelPosition = (width + 0.03) + ' ' + '0.03' + ' ' + (-depth + 0.27);
// var zLabelRotation = '-45' + ' ' + '-90' + ' ' + '0';
// d3.select('#' + originPointID)
// .append('a-entity')
// .attr('id', 'z')
// .attr('bmfont-text', 'text: Sepal Width (cm)')
// .attr('position', zLabelPosition)
// .attr('rotation', zLabelRotation);
// generate some random data
data.csv = [];
const colors = [ 'red', 'green', 'blue'];
// set the number of spheres in the scene
for (let i = 0; i < 48; i++) {
data.csv.push({
xV: Math.random(),
yV: Math.random(),
zV: Math.random(),
color: colors[Math.floor(Math.random() * 3)]
})
}
console.log('random data.csv', data.csv);
if (data.csv) {
/* Plot data from CSV */
var originPoint = d3.select('#originPoint' + data.id);
// Needed to assign species a color
var cScale = d3.scaleOrdinal()
.domain([
"red",
"blue",
"green"
])
.range([
'#d62728', // red
'#2ca02c', // green
'#1f77b4' // blue
]);
// Convert CSV data from string to number
// d3.csv(data.csv, function (data) {
// data.forEach(function (d) {
// d.color = cScale(d.color)
// });
// plotData(data);
// });
const plotDataOptions = {
xVariable: 'xV',
yVariable: 'yV',
zVariable: 'zV',
colorVariable: 'color'
}
plotData(data.csv, plotDataOptions);
function plotData(data, options) {
const xVariable = options.xVariable;
const yVariable = options.yVariable;
const zVariable = options.zVariable;
const colorVariable = options.colorVariable;
// Scale x, y, and z values
var xExtent = d3.extent(data, function (d) { return d[xVariable]; });
var xScale = d3.scaleLinear()
.domain(xExtent)
.range([xRange[0], xRange[1]])
.clamp('true');
var yExtent = d3.extent(data, function (d) { return d[yVariable]; });
var yScale = d3.scaleLinear()
.domain(yExtent)
.range([yRange[0], yRange[1]]);
var zExtent = d3.extent(data, function (d) { return d[zVariable] });
var zScale = d3.scaleLinear()
.domain(zExtent)
.range([zRange[0], zRange[1]]);
// Append data to graph and attach event listeners
originPoint.selectAll('a-sphere')
.data(data)
.enter()
.append('a-sphere')
.attr('radius', 0.05)
.attr('color', function(d) {
return d.color;
})
.attr('position', function (d) {
return xScale(d[xVariable]) + ' ' + yScale(d[yVariable]) + ' ' + zScale(d[zVariable]);
})
.attr('dynamic-body', '')
.classed('throwable', true)
.on('mouseenter', mouseEnter);
/**
* Event listener adds and removes data labels.
* "this" refers to sphere element of a given data point.
*/
function mouseEnter () {
// Get data
var data = this.__data__;
// Get width of graphBox (needed to set label position)
var graphBoxEl = this.parentElement.parentElement;
var graphBoxData = graphBoxEl.components.graph.data;
var graphBoxWidth = graphBoxData.width;
// Look for an existing label
var oldLabel = d3.select('#tempDataLabel');
var oldLabelParent = oldLabel.select(function () { return this.parentNode; });
// Look for an existing beam
var oldBeam = d3.select('#tempDataBeam');
// Look for an existing background
var oldBackground = d3.select('#tempDataBackground');
// If there is no existing label, make one
if (oldLabel[0][0] === null) {
labelMaker(this, graphBoxWidth);
} else {
// Remove old label
oldLabel.remove();
// Remove beam
oldBeam.remove();
// Remove background
oldBackground.remove();
// Create new label
labelMaker(this, graphBoxWidth);
}
}
};
}
}
});
/* HELPER FUNCTIONS */
/**
* planeMaker() creates a plane given width and height (kind of).
* It is used by gridMaker().
*/
function planeMaker (horizontal, vertical) {
// Controls texture repeat for U and V
var uHorizontal = horizontal * 4;
var vVertical = vertical * 4;
// Load a texture, set wrap mode to repeat
var texture = new THREE.TextureLoader().load('https://cdn.rawgit.com/bryik/aframe-scatter-component/master/assets/grid.png');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.anisotropy = 16;
texture.repeat.set(uHorizontal, vVertical);
// Create material and geometry
var material = new THREE.MeshBasicMaterial({map: texture, side: THREE.DoubleSide});
var geometry = new THREE.PlaneGeometry(horizontal, vertical);
return new THREE.Mesh(geometry, material);
}
/**
* gridMaker() creates a graphing box given width, height, and depth.
* The textures are also scaled to these dimensions.
*
* There are many ways this function could be improved or done differently
* e.g. buffer geometry, merge geometry, better reuse of material/geometry.
*/
function gridMaker (width, height, depth) {
var grid = new THREE.Object3D();
// AKA bottom grid
var xGrid = planeMaker(width, depth);
xGrid.rotation.x = 90 * (Math.PI / 180);
grid.add(xGrid);
// AKA far grid
var yPlane = planeMaker(width, height);
yPlane.position.y = (0.5) * height;
yPlane.position.z = (-0.5) * depth;
grid.add(yPlane);
// AKA side grid
var zPlane = planeMaker(depth, height);
zPlane.position.x = (-0.5) * width;
zPlane.position.y = (0.5) * height;
zPlane.rotation.y = 90 * (Math.PI / 180);
grid.add(zPlane);
return grid;
}
/**
* labelMaker() creates a label for a given data point and graph height.
* dataEl - A data point's element.
* graphBoxWidth - The width of the graph.
*/
function labelMaker (dataEl, graphBoxWidth, options) {
const xVariable = options.xVariable;
const yVariable = options.yVariable;
const zVariable = options.zVariable;
const xLabel = options.xVariable;
const yLabel = options.yVariable;
const zLabel = options.zVariable;
var dataElement = d3.select(dataEl);
// Retrieve original data
var dataValues = dataEl.__data__;
// Create individual x, y, and z labels using original data values
// round to 1 decimal space (should use d3 format for consistency later)
var xLabelText = 'xLabel: ' + dataValues[xVariable] + '\n \n';
var yLabelText = 'yLabel: ' + dataValues[yVariable] + '\n \n';
var zLabelText = 'zLabel: ' + dataValues[zVariable];
var labelText = 'text: ' + xLabelText + yLabelText + zLabelText;
// Position label right of graph
var padding = 0.2;
var sphereXPosition = dataEl.getAttribute('position').x;
var labelXPosition = (graphBoxWidth + padding) - sphereXPosition;
var labelPosition = labelXPosition + ' -0.43 0';
// Add pointer
var beamWidth = labelXPosition;
// The beam's pivot is in the center
var beamPosition = (labelXPosition - (beamWidth / 2)) + '0 0';
dataElement.append('a-box')
.attr('id', 'tempDataBeam')
.attr('height', '0.01')
.attr('width', beamWidth)
.attr('depth', '0.01')
.attr('color', 'purple')
.attr('position', beamPosition);
// Add label
dataElement.append('a-entity')
.attr('id', 'tempDataLabel')
.attr('bmfont-text', labelText)
.attr('position', labelPosition);
var backgroundPosition = (labelXPosition + 1.15) + ' 0.02 -0.1';
// Add background card
dataElement.append('a-plane')
.attr('id', 'tempDataBackground')
.attr('width', '2.3')
.attr('height', '1.3')
.attr('color', '#ECECEC')
.attr('position', backgroundPosition);
}