A Simplex, Hypercube and Cross Polytope from Paul Bourke's Hyperspace User Manual.
Visualized with hypersolid.js by Miłosz Kośmider.
A Simplex, Hypercube and Cross Polytope from Paul Bourke's Hyperspace User Manual.
Visualized with hypersolid.js by Miłosz Kośmider.
| /* | |
| * Hypersolid, Four-dimensional solid viewer | |
| * | |
| * Copyright (c) 2014 Milosz Kosmider <[email protected]> | |
| * | |
| * 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); | |
| } | |
| } | |
| }; | |
| 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. */ | |
| })(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>Hypersolids</title> | |
| <style> | |
| html, body { | |
| background: #fff; | |
| color: #555; | |
| width: 960px; | |
| margin: 0 auto; | |
| font-family: sans-serif; | |
| } | |
| canvas { | |
| border: none; | |
| display: inline-block; | |
| margin: 0 20px; | |
| } | |
| #hypercube-options { | |
| margin: 60px 0 0 85px; | |
| } | |
| label { | |
| margin: 0 20px; | |
| font-size: 15px; | |
| cursor: pointer; | |
| } | |
| h4 { | |
| display: inline-block; | |
| width: 300px; | |
| text-align:center; | |
| font-size: 15px; | |
| margin: 60px 0 15px 0; | |
| } | |
| </style> | |
| <script type="text/javascript" src="hypersolid.js"></script> | |
| <script type="text/javascript" src="hypersolid.shapebank.js"></script> | |
| </head> | |
| <body> | |
| <h4>Simplex</h4> | |
| <h4>Tesseract</h4> | |
| <h4>Orthoplex</h4> | |
| <canvas id="simplex-canvas">Unfortunately, your browser does not support coolness.</canvas> | |
| <canvas id="hypercube-canvas">Unfortunately, your browser does not support coolness.</canvas> | |
| <canvas id="cross-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 simplex = Hypersolid.Simplex(); | |
| var simplexView = Hypersolid.Viewport(simplex, document.getElementById('simplex-canvas'), { | |
| width: 260, | |
| height: 260, | |
| scale: 2, | |
| lineWidth: 3, | |
| lineJoin: 'round' | |
| }); | |
| simplexView.draw(); | |
| var cube = Hypersolid.Hypercube(); | |
| var cubeView = Hypersolid.Viewport(cube, document.getElementById('hypercube-canvas'), { | |
| width: 260, | |
| height: 260, | |
| scale: 2, | |
| lineWidth: 3, | |
| lineJoin: 'round' | |
| }); | |
| cubeView.draw(); | |
| var cross = Hypersolid.Cross(); | |
| var crossView = Hypersolid.Viewport(cross, document.getElementById('cross-canvas'), { | |
| width: 260, | |
| height: 260, | |
| scale: 2, | |
| lineWidth: 3, | |
| lineJoin: 'round' | |
| }); | |
| crossView.draw(); | |
| // starting rotation | |
| rotate("xz", 0.35); | |
| rotate("yz", 0.25); | |
| // 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) { | |
| simplex.rotate(plane, x); | |
| simplexView.draw(); | |
| cube.rotate(plane, x); | |
| cubeView.draw(); | |
| cross.rotate(plane, x); | |
| crossView.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> |