Skip to content

Instantly share code, notes, and snippets.

@syntagmatic
Last active August 27, 2015 12:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save syntagmatic/a6a51f04e61ff21341f2 to your computer and use it in GitHub Desktop.
Save syntagmatic/a6a51f04e61ff21341f2 to your computer and use it in GitHub Desktop.
Octaplex
/*
* Hypersolid, Four-dimensional solid viewer
*
* Copyright (c) 2014 Milosz Kosmider <milosz@milosz.ca>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
(function(Hypersolid) {
/* Begin constants. */
DEFAULT_VIEWPORT_WIDTH = 480; // Width of canvas in pixels
DEFAULT_VIEWPORT_HEIGHT = 480; // Height of canvas in pixels
DEFAULT_VIEWPORT_SCALE = 2; // Maximum distance from origin (in math units) that will be displayed on the canvas
DEFAULT_VIEWPORT_FONT = 'italic 10px sans-serif';
DEFAULT_VIEWPORT_FONT_COLOR = '#000';
DEFAULT_VIEWPORT_LINE_WIDTH = 4;
DEFAULT_VIEWPORT_LINE_JOIN = 'round';
DEFAULT_CHECKBOX_VALUES = {
perspective: { checked: true },
indices: { checked: false },
edges: { checked: true }
};
/* End constants. */
/* Begin classes. */
Hypersolid.Shape = function() {
return new Shape(Array.prototype.slice.call(arguments, 0));
};
function Shape(argv) {
var self = this,
vertices = argv[0],
edges = argv[1];
// Rotations will always be relative to the original shape to avoid rounding errors.
// This is a structure for caching the rotated vertices.
var rotatedVertices = new Array(vertices.length);
copyVertices();
// This is where we store the current rotations about each axis.
var rotations = { xy: 0, xz: 0, xw: 0, yz: 0, yw: 0, zw: 0 };
var rotationOrder = {
yz: 1,
xw: 1,
yw: 1,
zw: 1,
xy: 1,
xz: 1,
};
// Multiplication by vector rotation matrices of dimension 4
var rotateVertex = {
xy: function(v, s, c) {
tmp = c * v.x + s * v.y;
v.y = -s * v.x + c * v.y;
v.x = tmp;
},
xz: function(v, s, c) {
tmp = c * v.x + s * v.z;
v.z = -s * v.x + c * v.z;
v.x = tmp;
},
xw: function(v, s, c) {
tmp = c * v.x + s * v.w;
v.w = -s * v.x + c * v.w;
v.x = tmp;
},
yz: function(v, s, c) {
tmp = c * v.y + s * v.z;
v.z = -s * v.y + c * v.z;
v.y = tmp;
},
yw: function(v, s, c) {
tmp = c * v.y - s * v.w;
v.w = s * v.y + c * v.w;
v.y = tmp;
},
zw: function(v, s, c) {
tmp = c * v.z - s * v.w;
v.w = s * v.z + c * v.w;
v.z = tmp;
}
};
var eventCallbacks = {};
self.getOriginalVertices = function() {
return vertices;
};
self.getVertices = function() {
return rotatedVertices;
};
self.getEdges = function() {
return edges;
};
self.getRotations = function() {
return rotations;
};
// This will copy the original shape and put a rotated version into rotatedVertices
self.rotate = function(axis, theta) {
addToRotation(axis, theta);
applyRotations();
triggerEventCallbacks('rotate');
};
self.on = function(eventName, callback) {
if (eventCallbacks[eventName] === undefined) {
eventCallbacks[eventName] = [];
}
eventCallbacks[eventName].push(callback);
};
function triggerEventCallbacks(eventName) {
if (eventCallbacks[eventName] !== undefined) {
for (index in eventCallbacks[eventName]) {
eventCallbacks[eventName][index].call(self);
}
}
}
function addToRotation(axis, theta) {
rotations[axis] = (rotations[axis] + theta) % (2 * Math.PI);
}
function applyRotations() {
copyVertices();
for (var axis in rotationOrder) {
// sin and cos precomputed for efficiency
var s = Math.sin(rotations[axis]);
var c = Math.cos(rotations[axis]);
for (var i in vertices)
{
rotateVertex[axis](rotatedVertices[i], s, c);
}
}
}
function copyVertices() {
for (var i in vertices) {
var vertex = vertices[i];
rotatedVertices[i] = {
x: vertex.x,
y: vertex.y,
z: vertex.z,
w: vertex.w
};
}
}
}
Hypersolid.Viewport = function() {
return new Viewport(Array.prototype.slice.call(arguments, 0));
};
function Viewport(argv) {
var self = this,
shape = argv[0],
canvas = argv[1],
options = argv[2];
options = options || {};
var scale = options.scale || DEFAULT_VIEWPORT_SCALE;
canvas.width = options.width || DEFAULT_VIEWPORT_WIDTH;
canvas.height = options.height || DEFAULT_VIEWPORT_HEIGHT;
var bound = Math.min(canvas.width, canvas.height) / 2;
var context = canvas.getContext('2d');
context.font = options.font || DEFAULT_VIEWPORT_FONT;
context.textBaseline = 'top';
context.fillStyle = options.fontColor || DEFAULT_VIEWPORT_FONT_COLOR;
context.lineWidth = options.lineWidth || DEFAULT_VIEWPORT_LINE_WIDTH;
context.lineJoin = options.lineJoin || DEFAULT_VIEWPORT_LINE_JOIN;
var checkboxes = options.checkboxes || DEFAULT_CHECKBOX_VALUES;
var clicked = false;
var startCoords;
self.draw = function() {
var vertices = shape.getVertices();
var edges = shape.getEdges();
context.clearRect(0, 0, canvas.width, canvas.height);
var adjusted = [];
for (var i in vertices) {
if (checkboxes.perspective.checked) {
var zratio = vertices[i].z / scale;
adjusted[i] = {
x: Math.floor(canvas.width / 2 + (0.90 + zratio * 0.30) * bound * (vertices[i].x / scale)) + 0.5,
y: Math.floor(canvas.height / 2 - (0.90 + zratio * 0.30) * bound * (vertices[i].y / scale)) + 0.5,
z: 0.50 + 0.40 * zratio,
w: 121 + Math.floor(134 * vertices[i].w / scale)
};
}
else {
adjusted[i] = {
x: Math.floor(canvas.width / 2 + bound * (vertices[i].x / scale)) + 0.5,
y: Math.floor(canvas.height / 2 - bound * (vertices[i].y / scale)) + 0.5,
z: 0.50 + 0.40 * vertices[i].z / scale,
w: 121 + Math.floor(134 * vertices[i].w / scale)
};
}
}
if (checkboxes.edges.checked) {
for (var i in edges) {
var x = [adjusted[edges[i][0]].x, adjusted[edges[i][1]].x];
var y = [adjusted[edges[i][0]].y, adjusted[edges[i][1]].y];
var z = [adjusted[edges[i][0]].z, adjusted[edges[i][1]].z];
var w = [adjusted[edges[i][0]].w, adjusted[edges[i][1]].w];
context.beginPath();
context.moveTo(x[0], y[0]);
context.lineTo(x[1], y[1]);
context.closePath();
var gradient = context.createLinearGradient(x[0], y[0], x[1], y[1]); // Distance fade effect
gradient.addColorStop(0, 'rgba(' + w[0] + ',94,' + (125-Math.round(w[0]/2)) +', ' + z[0] + ')');
gradient.addColorStop(1, 'rgba(' + w[1] + ',94,' + (125-Math.round(w[0]/2)) +', ' + z[1] + ')');
context.strokeStyle = gradient;
context.stroke();
}
}
if (checkboxes.indices.checked) {
for (var i in adjusted) {
context.fillText(i.toString(), adjusted[i].x, adjusted[i].y);
}
}
};
canvas.onmousedown = function(e) {
startCoords = mouseCoords(e, canvas);
startCoords.x -= Math.floor(canvas.width / 2);
startCoords.y = Math.floor(canvas.height / 2) - startCoords.y;
clicked = true;
};
document.onmousemove = function(e) {
if (!clicked) {
return true;
}
var currCoords = mouseCoords(e, canvas);
currCoords.x -= Math.floor(canvas.width / 2);
currCoords.y = Math.floor(canvas.height / 2) - currCoords.y;
var motion = { 'x': currCoords.x - startCoords.x, 'y': currCoords.y - startCoords.y };
if (e.shiftKey && (e.altKey || e.ctrlKey)) {
shape.rotate('xy', Math.PI * motion.x / bound); // Full canvas drag ~ 2*PI
shape.rotate('zw', Math.PI * motion.y / bound);
}
else if (e.shiftKey) {
// Interpretation of this rotation varies between left- and right- brained users
shape.rotate('xw', Math.PI * motion.x / bound);
shape.rotate('yw', Math.PI * motion.y / bound);
}
else {
shape.rotate('xz', Math.PI * motion.x / bound);
shape.rotate('yz', Math.PI * motion.y / bound);
}
startCoords = currCoords;
self.draw();
};
document.onmouseup = function() {
clicked = false;
};
checkboxes.onchange = function() {
self.draw();
};
}
/* End classes. */
/* Begin methods. */
// parse ascii files from http://paulbourke.net/geometry/hyperspace/
Hypersolid.parseVEF = function(text) {
var lines = text.split("\n");
var nV = parseInt(lines[0]); // number of vertices
var nE = parseInt(lines[1+nV]); // number of edges
var nF = parseInt(lines[2+nV+nE]); // number of faces
var vertices = lines.slice(1,1+nV).map(function(line) {
var d = line.split("\t").map(parseFloat);
return {
x: d[0],
y: d[1],
z: d[2],
w: d[3],
}
});
var edges = lines.slice(2+nV,2+nV+nE).map(function(line) {
var d = line.replace("\s","").split("\t").map(function(vertex) { return parseInt(vertex); });
return [d[0], d[1]];;
});
var faces = lines.slice(3+nV+nE,3+nV+nE+nF).map(function(line) {
var d = line.replace("\s","").split("\t").map(function(edge) { return parseInt(edge); });
return d;
});
return [vertices,edges,faces]
};
/* End methods. */
/* Begin helper routines. */
function mouseCoords(e, element) { // http://answers.oreilly.com/topic/1929-how-to-use-the-canvas-and-draw-elements-in-html5/
var x;
var y;
if (e.pageX || e.pageY) {
x = e.pageX;
y = e.pageY;
}
else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= element.offsetLeft;
y -= element.offsetTop;
return { 'x': x, 'y': y };
}
/* End helper routines. */
})(window.Hypersolid = window.Hypersolid || {});
(function(Hypersolid) {
/*
* Hypercube
*/
Hypersolid.Hypercube = function() {
return new Hypercube();
};
function Hypercube() {};
Hypercube.prototype = Hypersolid.Shape([
{ x: 1, y: 1, z: 1, w: 1 },
{ x: 1, y: 1, z: 1, w: -1 },
{ x: 1, y: 1, z: -1, w: 1 },
{ x: 1, y: 1, z: -1, w: -1 },
{ x: 1, y: -1, z: 1, w: 1 },
{ x: 1, y: -1, z: 1, w: -1 },
{ x: 1, y: -1, z: -1, w: 1 },
{ x: 1, y: -1, z: -1, w: -1 },
{ x: -1, y: 1, z: 1, w: 1 },
{ x: -1, y: 1, z: 1, w: -1 },
{ x: -1, y: 1, z: -1, w: 1 },
{ x: -1, y: 1, z: -1, w: -1 },
{ x: -1, y: -1, z: 1, w: 1 },
{ x: -1, y: -1, z: 1, w: -1 },
{ x: -1, y: -1, z: -1, w: 1 },
{ x: -1, y: -1, z: -1, w: -1 }
], [
[ 0, 1], [ 0, 2], [ 0, 4], [ 0, 8],
[ 1, 3], [ 1, 5], [ 1, 9],
[ 2, 3], [ 2, 6], [ 2, 10],
[ 3, 7], [ 3, 11],
[ 4, 5], [ 4, 6], [ 4, 12],
[ 5, 7], [ 5, 13],
[ 6, 7], [ 6, 14],
[ 7, 15],
[ 8, 9], [ 8, 10], [ 8, 12],
[ 9, 11], [ 9, 13],
[10, 11], [10, 14],
[11, 15],
[12, 13], [12, 14],
[13, 15],
[14, 15]
]);
// 5 cell
Hypersolid.Simplex = function() {
return new Simplex();
};
function Simplex() {};
Simplex.prototype = Hypersolid.Shape([
{"x":0,"y":0,"z":0,"w":2},
{"x":-1,"y":1,"z":1,"w":0},
{"x":1,"y":-1,"z":1,"w":0},
{"x":1,"y":1,"z":-1,"w":0},
{"x":-1,"y":-1,"z":-1,"w":0}
], [
[0,1],[0,2],[0,3],
[1,2],[1,3],
[2,3],
[3,4],
[4,0],[4,1],[4,2],
]);
// 16 cell
Hypersolid.Cross = function() {
return new Cross();
};
function Cross() {};
Cross.prototype = Hypersolid.Shape([
{"x":-2,"y":0,"z":0,"w":0},
{"x":0,"y":-2,"z":0,"w":0},
{"x":0,"y":0,"z":-2,"w":0},
{"x":0,"y":0,"z":0,"w":-2},
{"x":2,"y":0,"z":0,"w":0},
{"x":0,"y":2,"z":0,"w":0},
{"x":0,"y":0,"z":2,"w":0},
{"x":0,"y":0,"z":0,"w":2}
], [
[0,1],[0,2],[0,3],[0,5],[0,6],
[1,2],[1,3],[1,4],[1,6],
[2,3],[2,4],[2,5],
[3,4],[3,5],
[4,5],[4,6],
[5,6],
[6,3],[6,7],
[7,0],[7,1],[7,2],[7,4],[7,5]
]);
// 24 cell
Hypersolid.Icositetrachoron = function() {
return new Icositetrachoron();
};
function Icositetrachoron() {};
Icositetrachoron.prototype = Hypersolid.Shape([
{x:-2,y:0,z:0,w:0},
{x:0,y:-2,z:0,w:0},
{x:0,y:0,z:-2,w:0},
{x:0,y:0,z:0,w:-2},
{x:2,y:0,z:0,w:0},
{x:0,y:2,z:0,w:0},
{x:0,y:0,z:2,w:0},
{x:0,y:0,z:0,w:2},
{x:-1,y:-1,z:-1,w:-1},
{x:-1,y:-1,z:-1,w:1},
{x:-1,y:-1,z:1,w:-1},
{x:-1,y:-1,z:1,w:1},
{x:-1,y:1,z:-1,w:-1},
{x:-1,y:1,z:-1,w:1},
{x:-1,y:1,z:1,w:-1},
{x:-1,y:1,z:1,w:1},
{x:1,y:-1,z:-1,w:-1},
{x:1,y:-1,z:-1,w:1},
{x:1,y:-1,z:1,w:-1},
{x:1,y:-1,z:1,w:1},
{x:1,y:1,z:-1,w:-1},
{x:1,y:1,z:-1,w:1},
{x:1,y:1,z:1,w:-1},
{x:1,y:1,z:1,w:1}
], [
[0,8],
[10,0],[10,1],[10,11],[10,14],[10,18],[10,3],
[11,0],[11,1],[11,15],[11,19],[11,6],[11,7],
[12,0],[12,13],[12,14],[12,2],[12,20],[12,3],
[13,0],[13,15],[13,2],[13,21],[13,5],[13,7],
[14,0],[14,15],[14,22],[14,3],[14,5],[14,6],
[15,0],[15,23],[15,5],[15,6],[15,7],
[16,1],[16,17],[16,18],[16,2],[16,20],[16,3],
[17,1],[17,19],[17,2],[17,21],[17,4],[17,7],
[1,8],
[18,1],[18,19],[18,22],[18,3],[18,4],[18,6],
[19,1],[19,23],[19,4],[19,6],[19,7],
[20,2],[20,21],[20,22],[20,3],[20,4],[20,5],
[21,2],[21,23],[21,4],[21,5],[21,7],
[22,23],[22,3],[22,4],[22,5],[22,6],
[23,4],[23,5],[23,6],[23,7],
[2,8],[3,8],
[4,16],
[5,12],
[6,10],
[7,9],
[8,10],[8,12],[8,16],[8,9],
[9,0],[9,1],[9,11],[9,13],[9,17],[9,2]
]);
})(window.Hypersolid = window.Hypersolid || {});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Octaplex</title>
<style>
html, body {
background: #fff;
color: #555;
width: 960px;
margin: 0 auto;
font-family: sans-serif;
}
canvas {
border: none;
margin: 0 280px;
}
#hypercube-options {
margin: 10px 0 0 85px;
}
label {
margin: 0 20px;
font-size: 15px;
cursor: pointer;
}
</style>
<script type="text/javascript" src="hypersolid.js"></script>
<script type="text/javascript" src="hypersolid.shapebank.js"></script>
</head>
<body>
<canvas id="octaplex-canvas">Unfortunately, your browser does not support coolness.</canvas>
<form id="hypercube-options">
<label><input type="checkbox" name="rotate_xy" />Rotate xy</label>
<label><input type="checkbox" name="rotate_yz" />Rotate yz</label>
<label><input type="checkbox" name="rotate_xz" />Rotate xz</label>
<label><input type="checkbox" name="rotate_xw" />Rotate xw</label>
<label><input type="checkbox" name="rotate_yw" />Rotate yw</label>
<label><input type="checkbox" name="rotate_zw" />Rotate zw</label>
</form>
<script type="text/javascript">
var octaplex = Hypersolid.Icositetrachoron();
var octaplexView = Hypersolid.Viewport(octaplex, document.getElementById('octaplex-canvas'), {
width: 440,
height: 440,
scale: 2,
lineWidth: 3,
lineJoin: 'round'
});
octaplexView.draw();
// animation
options = document.getElementById('hypercube-options');
function render() {
if (options) {
checkboxes = options.getElementsByTagName('input');
}
if (options.rotate_xz.checked) {
rotate("xz", 0.008);
}
if (options.rotate_yz.checked) {
rotate("yz", 0.008);
}
if (options.rotate_xw.checked) {
rotate("xw", 0.008);
}
if (options.rotate_yw.checked) {
rotate("yw", 0.008);
}
if (options.rotate_xy.checked) {
rotate("xy", 0.008);
}
if (options.rotate_zw.checked) {
rotate("zw", 0.008);
}
};
function rotate(plane, x) {
octaplex.rotate(plane, x);
octaplexView.draw();
};
window.requestAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
(function animloop(){
requestAnimFrame(animloop);
render();
})();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment