window.onerror = function( message ) { document.getElementById( window.id + 'output' ).innerHTML = message; } function MathCell( id, inputs, config={} ) { function labeledInteract( input ) { var label = 'label' in input ? input.label : ''; if ( label.length === 1 ) label = `${label}`; if ( input.type === 'action' ) return `
${interact( id, input )}
`; return `
${label}
${interact( id, input )}
`; } function inputTable( inputs ) { var t = ''; if ( !Array.isArray(inputs[0]) ) inputs = [ inputs ]; inputs.forEach( row => { t += ''; row.forEach( column => { t += ''; if ( Array.isArray(column) ) t += inputTable( column ); else t += labeledInteract( column ); t += ''; } ); t += ''; } ); return ` ${t}
`; } var s = ''; // process array of dictionaries for ( var i = 0 ; i < inputs.length ; i++ ) { var input = inputs[i]; if ( Array.isArray(input) ) s += inputTable( input ); else s += labeledInteract( input ); } s += `
`; var outputIndex = 1; function outputTable( outputs ) { // table inside flex box grows on each update, divs do not! var t = ''; if ( !Array.isArray(outputs[0]) ) outputs = [ outputs ]; outputs.forEach( row => { t += `
`; row.forEach( column => { if ( Array.isArray(column) ) t += outputTable( column ); else { t += `
`; outputIndex++; } } ); t += `
`; } ); return `
${t}
`; } if ( 'multipleOutputs' in config ) s += outputTable( config.multipleOutputs ); else s += `
`; s += `
`; var cell = document.createRange().createContextualFragment( s ) document.getElementById( id ).appendChild( cell ); } function interact( id, input ) { var name = 'name' in input ? input.name : ''; switch ( input.type ) { case 'slider': var min = 'min' in input ? input.min : 0; var max = 'max' in input ? input.max : 1; var step = 'step' in input ? input.step : .01; var value = 'default' in input ? input.default : min; return ` `; case 'buttons': var values = 'values' in input ? input.values : [1,2,3]; var labels = 'labels' in input ? input.labels : false; var select = 'default' in input ? input.default : values[0]; var style = input.width ? 'style="width: ' + input.width + '"' : ''; var s = ''; for ( var i = 0 ; i < values.length ; i++ ) s += `   `; return s; case 'number': var min = 'min' in input ? input.min : 0; var max = 'max' in input ? input.max : 1; var step = 'step' in input ? input.step : .01; var value = 'default' in input ? input.default : min; return ` `; case 'checkbox': var checked = 'default' in input ? input.default : ''; return ` `; case 'text': var value = 'default' in input ? input.default : ''; var width = 'width: ' + ( input.width ? input.width : 'calc(100% - .6in)' ); return ` `; case 'iterator': var value = 'default' in input ? input.default : 0; var width = '.75in', last = ''; if ( input.reversible ) last = ` `; return `
${value}
${last} `; case 'action': var script = 'script' in input ? input.script : 'doNothing'; var label = 'label' in input ? input.label : ' '; var width = 'width' in input ? input.width : '1in'; if ( input.subtype === 'updateParent' ) return ` `; default: return 'Unsupported input type'; } } function graphic( id, data, config ) { switch ( config.type ) { case 'svg': return svg( id, data, config ); case 'threejs': return threejs( id, data, config ); case 'x3d': return x3d( id, data, config ); case 'text': // need JSON stringify to render objects // explicit double quotes removed by default // if needed in output use " var center = config.center ? 'text-align: center' : ''; return `
${JSON.stringify( data ).replace( /\"/g, '' )}
`; case 'matrix': var s = ` `; for ( var i = 0 ; i < data.length ; i++ ) { s += ''; for ( var j = 0 ; j < data[i].length ; j++ ) { s += ''; } s += ''; } s += '
' + data[i][j] + '
'; var leftBracket = ` `; var rightBracket = ` `; return `
${leftBracket} ${s} ${rightBracket}
`; default: return 'Unsupported graphic type'; } } function generateId() { return 'id' + Math.floor( 10**10 * Math.random() ); } function checkLimits( input ) { if ( +input.value < +input.min ) input.value = input.min; if ( +input.value > +input.max ) input.value = input.max; } function getVariable( id, name ) { // plus sign invokes Number object to ensure numeric result // input type already validated on creation var input = document.getElementById( id + name ); if ( input ) { if ( input.innerHTML ) return +input.innerHTML; // iterator switch ( input.type ) { case 'number': case 'range': return +input.value; case 'checkbox': return input.checked; case 'text': return input.value; } } else { var value = document.querySelector( 'input[name=' + id + name + ']:checked' ).value; if ( isNaN(value) ) return value; return +value; } } function setLimit( id, name, end, value ) { var input = document.getElementById( id + name ); switch( end ) { case 'min' : input.min = value; if ( input.value < value ) input.value = value; break; case 'max' : input.max = value; if ( input.value > value ) input.value = value; } if ( input.type === 'range' ) { // update slider box var box = document.getElementById( id + name + 'Box' ); box.min = input.min; box.max = input.max; box.value = input.value; } } function evaluate( id, data, config ) { var outputs = document.querySelectorAll( '[id^=' + id + 'output]' ); if ( outputs.length === 1 ) { var output = outputs[0]; output.innerHTML = graphic( id, data, config ); if ( config.type === 'threejs' ) iOSFix( output ); } else { for ( var i = 0 ; i < outputs.length ; i ++ ) { var output = outputs[i]; var n = output.id.substr( output.id.indexOf('output') + 6 ); var c = Array.isArray(config) ? config[i] : JSON.parse( JSON.stringify( config ) ); c.output = n; c.no3DBorder = true; output.innerHTML = graphic( id, data[i], c ); if ( c.type === 'threejs' ) iOSFix( output ); if ( c.type === 'matrix' ) output.style.border = 'none'; } } function iOSFix( output ) { var iframe = output.children[0]; if ( /(iPad|iPhone|iPod)/g.test( navigator.userAgent ) ) { iframe.style.width = getComputedStyle( iframe ).width; iframe.style.height = getComputedStyle( iframe ).height; } } } function injectFunctions( id, functions, n='' ) { var output = document.getElementById( id + 'output' + n ); if ( output.children.length > 0 && output.children[0].contentWindow ) { var cw = output.children[0].contentWindow; Object.keys( functions ).forEach( k => cw[k] = functions[k] ); } else throw Error( 'injectFuctions must follow evaluate' ); } function minMax( d, index ) { var min = Number.MAX_VALUE; var max = -Number.MAX_VALUE; for ( var i = 0 ; i < d.length ; i++ ) { if ( d[i][index] < min ) min = d[i][index]; if ( d[i][index] > max ) max = d[i][index]; } return { min: min, max: max }; } var numericInfinity = 1e300; function dataReplacer( key, value ) { if ( value === Infinity ) return 'Infinity'; if ( value === -Infinity ) return '-Infinity'; if ( value === undefined ) return 'NaN'; if ( value !== value ) return 'NaN'; return value; } function dataReviver( key, value ) { if ( value === 'Infinity' ) return numericInfinity; if ( value === '-Infinity' ) return -numericInfinity; if ( value === 'NaN' ) return NaN; return value; } function linspace( a, b, points ) { var result = []; var step = ( b - a ) / ( points - 1 ); for ( var i = 0 ; i < points - 1 ; i++ ) result.push( a + i * step ); result.push( b ); return result; } function lerp( a, b ) { return function( x ) { return a[1] + ( x - a[0] ) * ( b[1] - a[1] ) / ( b[0] - a[0] ) }; } // rounding functions function roundTo( x, n, significant=true ) { if ( x === 0 ) return x; if ( Array.isArray(x) ) { var v = []; for ( var i = 0 ; i < x.length ; i++ ) v[i] = roundTo( x[i], n, significant ); return v; } if ( significant ) { var exponent = Math.floor( Math.log10( Math.abs(x) ) ); n = n - exponent - 1; } return Math.round( 10**n * x ) / 10**n; } function ceilTo( x, n, significant=true ) { if ( x === 0 ) return x; if ( Array.isArray(x) ) { var v = []; for ( var i = 0 ; i < x.length ; i++ ) v[i] = ceilTo( x[i], n, significant ); return v; } if ( significant ) { var exponent = Math.floor( Math.log10( Math.abs(x) ) ); n = n - exponent - 1; } return Math.ceil( 10**n * x ) / 10**n; } function floorTo( x, n, significant=true ) { if ( x === 0 ) return x; if ( Array.isArray(x) ) { var v = []; for ( var i = 0 ; i < x.length ; i++ ) v[i] = floorTo( x[i], n, significant ); return v; } if ( significant ) { var exponent = Math.floor( Math.log10( Math.abs(x) ) ); n = n - exponent - 1; } return Math.floor( 10**n * x ) / 10**n; } // transformation functions function normalize( vector ) { var len = Math.hypot.apply( null, vector ); for ( var i = 0 ; i < vector.length ; i++ ) vector[i] /= len; return vector; } function translate( points, vector ) { for ( var i = 0 ; i < points.length ; i++ ) for ( var j = 0 ; j < vector.length ; j++ ) points[i][j] += vector[j]; return points; } function rotate( points, angle=0, vector=[0,0,1] ) { // fails silently without defaults var dimension = points[0].length; switch( dimension ) { case 2: for ( var i = 0 ; i < points.length ; i++ ) { var v = points[i]; var x = v[0]*Math.cos(angle) - v[1]*Math.sin(angle); var y = v[0]*Math.sin(angle) + v[1]*Math.cos(angle); points[i] = [ x, y ]; } break; case 3: var norm = Math.hypot.apply( null, vector ); if ( norm === 0 ) break; if ( norm !== 1 ) for ( var i = 0 ; i < 3 ; i++ ) vector[i] /= norm; var n1 = vector[0]; var n2 = vector[1]; var n3 = vector[2]; var c = Math.cos(angle); var s = Math.sin(angle); // Rodrigues in matrix form var M = [ [ c + (1-c)*n1**2, -s*n3 + (1-c)*n1*n2, s*n2 + (1-c)*n1*n3 ], [ s*n3 + (1-c)*n1*n2, c + (1-c)*n2**2, -s*n1 + (1-c)*n2*n3 ], [ -s*n2 + (1-c)*n1*n3, s*n1 + (1-c)*n2*n3, c + (1-c)*n3**2 ] ]; for ( var i = 0 ; i < points.length ; i++ ) { var v = points[i]; var x = 0, y = 0, z = 0; for ( var j = 0 ; j < v.length ; j++ ) { x += M[0][j]*v[j]; y += M[1][j]*v[j]; z += M[2][j]*v[j]; } points[i] = [ x, y, z ]; } break; default: throw Error( 'Unsupported rotation dimension' ); } } function rotateFromZAxis( points, vector ) { var angle = Math.acos( vector[2] / Math.hypot.apply( null, vector ) ); rotate( points, angle, [ -vector[1], vector[0], 0 ] ); } function rotateObject( object, angle, vector ) { object.forEach( e => rotate( e.vertices, angle, vector ) ); } // presentation functions function getCompleteCode() { var cell = document.getElementsByClassName( 'mathcell' )[0] var copy = cell.cloneNode( false ); copy.removeAttribute( 'id' ); copy.appendChild( cell.children[0] ); var s = copy.outerHTML.replace( ' `; } function threejs( id, data, config ) { // working copy of data var data = JSON.parse( JSON.stringify( data, dataReplacer ), dataReviver ); if ( !( 'aspectRatio' in config ) ) config.aspectRatio = [1,1,1]; if ( !( 'axesLabels' in config ) || config.axesLabels === true ) config.axesLabels = ['x','y','z']; if ( !( 'clearColor' in config ) ) config.clearColor = 'white'; if ( !( 'decimals' in config ) ) config.decimals = 2; if ( !( 'frame' in config ) ) config.frame = true; if ( !( 'viewpoint' in config ) ) config.viewpoint = 'auto'; if ( !config.frame ) config.axesLabels = false; if ( !( 'lights' in config ) ) config.lights = [ { type: 'ambient', color: 'rgb(127,127,127)', intensity: 4 }, { type: 'directional', parent: 'camera', position: [-5,3,0], color: 'rgb(127,127,127)', intensity: 9 } ]; var n = 'output' in config ? config.output : ''; var output = document.getElementById( id + 'output' + n ); if ( output.children.length > 0 && output.children[0].contentWindow ) { var cw = output.children[0].contentWindow; var v = cw.camera.position; // only direction of viewpoint meaningful, not normalization config.viewpoint = [ v.x - cw.xMid, v.y - cw.yMid, v.z - cw.zMid ]; } var texts = [], points = [], lines = [], surfaces = []; for ( var i = 0 ; i < data.length ; i++ ) for ( var j = 0 ; j < data[i].length ; j++ ) { var d = data[i][j]; if ( d.type === 'text' ) { if ( typeof d.point[2] === 'undefined' ) d.point[2] = 0; texts.push( d ); } if ( d.type === 'point' ) { if ( typeof d.point[2] === 'undefined' ) d.point[2] = 0; points.push( d ); } if ( d.type === 'line' ) { d.points.forEach ( p => { if ( typeof p[2] === 'undefined' ) p[2] = 0; } ); d.points = roundTo( d.points, 3, false ); // reduce raw data size lines.push( d ); } if ( d.type === 'surface' ) { d.vertices = roundTo( d.vertices, 3, false ); // reduce raw data size surfaces.push( d ); } } var all = []; for ( var i = 0 ; i < texts.length ; i++ ) all.push( texts[i].point ); for ( var i = 0 ; i < points.length ; i++ ) all.push( points[i].point ); for ( var i = 0 ; i < lines.length ; i++ ) lines[i].points.forEach( p => all.push( p ) ); for ( var i = 0 ; i < surfaces.length ; i++ ) surfaces[i].vertices.forEach( p => all.push( p ) ); var xMinMax = minMax( all, 0 ); var yMinMax = minMax( all, 1 ); var zMinMax = minMax( all, 2 ); if ( !( 'xMin' in config ) ) config.xMin = xMinMax.min; if ( !( 'yMin' in config ) ) config.yMin = yMinMax.min; if ( !( 'zMin' in config ) ) config.zMin = zMinMax.min; if ( !( 'xMax' in config ) ) config.xMax = xMinMax.max; if ( !( 'yMax' in config ) ) config.yMax = yMinMax.max; if ( !( 'zMax' in config ) ) config.zMax = zMinMax.max; surfaces.forEach( s => { // process predefined colormaps if ( 'colormap' in s.options && ( !( 'colors' in s.options ) || s.options.colors.length === 0 ) ) { s.options.colors = []; var f = colormap( s.options.colormap, s.options.reverseColormap ); var zMinMax = minMax( s.vertices, 2 ); var zMin = zMinMax.min < config.zMin ? config.zMin : zMinMax.min; var zMax = zMinMax.max > config.zMax ? config.zMax : zMinMax.max; for ( var i = 0 ; i < s.vertices.length ; i++ ) { var z = s.vertices[i][2]; if ( z < zMin ) z = zMin; if ( z > zMax ) z = zMax; var w = ( z - zMin ) / ( zMax - zMin ); s.options.colors.push( f(w) ); } } } ); var border = config.no3DBorder ? 'none' : '1px solid black'; config = JSON.stringify( config ); texts = JSON.stringify( texts ); points = JSON.stringify( points ); lines = JSON.stringify( lines, dataReplacer ); surfaces = JSON.stringify( surfaces, dataReplacer ); var html = threejsTemplate( config, texts, points, lines, surfaces ); return ``; } function x3d( id, data, config ) { // working copy of data var data = JSON.parse( JSON.stringify( data, dataReplacer ), dataReviver ); function compositeRotation( first, second ) { var a = first[0], na = first[1]; var b = second[0], nb = second[1]; var dot = na[0]*nb[0] + na[1]*nb[1] + na[2]*nb[2]; var cross = [ na[1]*nb[2] - na[2]*nb[1], na[2]*nb[0] - na[0]*nb[2], na[0]*nb[1] - na[1]*nb[0] ]; var c = 2 * Math.acos( Math.cos(a/2) * Math.cos(b/2) - dot * Math.sin(a/2) * Math.sin(b/2) ); var nc = []; for ( var i = 0 ; i < 3 ; i++ ) nc[i] = na[i] * Math.sin(a/2) * Math.cos(b/2) / Math.sin(c/2) + nb[i] * Math.cos(a/2) * Math.sin(b/2) / Math.sin(c/2) - cross[i] * Math.sin(a/2) * Math.sin(b/2) / Math.sin(c/2); return [ c, nc ]; } var frame = 'frame' in config ? config.frame : true; var viewer = 'viewer' in config ? config.viewer : 'x3dom'; var n = 'output' in config ? config.output : ''; var output = document.getElementById( id + 'output' + n ); var width = output.offsetWidth; var height = output.offsetHeight; var texts = [], points = [], lines = [], surfaces = []; for ( var i = 0 ; i < data.length ; i++ ) for ( var j = 0 ; j < data[i].length ; j++ ) { var d = data[i][j]; if ( d.type === 'text' ) texts.push( d ); if ( d.type === 'point' ) points.push( d ); if ( d.type === 'line' ) lines.push( d ); if ( d.type === 'surface' ) { d.vertices = roundTo( d.vertices, 3, false ); // reduce raw data size surfaces.push( d ); } } var all = []; for ( var i = 0 ; i < texts.length ; i++ ) all.push( texts[i].point ); for ( var i = 0 ; i < points.length ; i++ ) all.push( points[i].point ); for ( var i = 0 ; i < lines.length ; i++ ) lines[i].points.forEach( p => all.push( p ) ); for ( var i = 0 ; i < surfaces.length ; i++ ) surfaces[i].vertices.forEach( p => all.push( p ) ); var xMinMax = minMax( all, 0 ); var yMinMax = minMax( all, 1 ); var zMinMax = minMax( all, 2 ); var xMin = 'xMin' in config ? config.xMin : xMinMax.min; var yMin = 'yMin' in config ? config.yMin : yMinMax.min; var zMin = 'zMin' in config ? config.zMin : zMinMax.min; var xMax = 'xMax' in config ? config.xMax : xMinMax.max; var yMax = 'yMax' in config ? config.yMax : yMinMax.max; var zMax = 'zMax' in config ? config.zMax : zMinMax.max; var xRange = xMax - xMin; var yRange = yMax - yMin; var zRange = zMax - zMin; var xMid = ( xMax + xMin ) / 2; var yMid = ( yMax + yMin ) / 2; var zMid = ( zMax + zMin ) / 2; var boxHelper = [ [ [xMin,yMin,zMin],[xMax,yMin,zMin] ], [ [xMin,yMin,zMin],[xMin,yMax,zMin] ], [ [xMin,yMin,zMin],[xMin,yMin,zMax] ], [ [xMax,yMin,zMin],[xMax,yMax,zMin] ], [ [xMax,yMin,zMin],[xMax,yMin,zMax] ], [ [xMin,yMax,zMin],[xMax,yMax,zMin] ], [ [xMin,yMax,zMin],[xMin,yMax,zMax] ], [ [xMin,yMin,zMax],[xMax,yMin,zMax] ], [ [xMin,yMin,zMax],[xMin,yMax,zMax] ], [ [xMax,yMax,zMin],[xMax,yMax,zMax] ], [ [xMax,yMin,zMax],[xMax,yMax,zMax] ], [ [xMin,yMax,zMax],[xMax,yMax,zMax] ] ]; // default orientation is looking down z-axis, even after displacement // need to rotate viewpoint back to origin with composite orientation var zRotation = [ Math.PI/2 + Math.atan(yRange/xRange), [ 0, 0, 1 ] ]; var norm1 = Math.sqrt( xRange**2 + yRange**2 + zRange**2 ); var norm2 = Math.sqrt( xRange**2 + yRange**2 ); var xyRotation = [ Math.acos( zRange/norm1 ), [ -yRange/norm2, xRange/norm2, 0 ] ]; var cr = compositeRotation( zRotation, xyRotation ); var x3d = ` `; if ( frame ) x3d += ` `; for ( var i = 0 ; i < surfaces.length ; i++ ) { var s = surfaces[i]; // remove faces completely outside vertical range for ( var j = s.faces.length - 1 ; j >= 0 ; j-- ) { var f = s.faces[j]; var check = true; f.forEach( index => check = check && s.vertices[index][2] < zMin ); if ( check ) s.faces.splice( j, 1 ); var check = true; f.forEach( index => check = check && s.vertices[index][2] > zMax ); if ( check ) s.faces.splice( j, 1 ); } // constrain vertices to vertical range for ( var j = 0 ; j < s.vertices.length ; j++ ) { if ( s.vertices[j][2] < zMin ) s.vertices[j][2] = zMin; if ( s.vertices[j][2] > zMax ) s.vertices[j][2] = zMax; } var indices = ''; for ( var j = 0 ; j < s.faces.length ; j++ ) indices += s.faces[j].join(' ') + ' -1 '; var points = ''; for ( var j = 0 ; j < s.vertices.length ; j++ ) points += s.vertices[j].join(' ') + ' '; var p = document.createElement( 'p' ); p.style.color = s.options.color; var rgb = p.style.color.replace( /[^\d,]/g, '' ).split(','); rgb.forEach( (e,i,a) => a[i] /= 255 ); var color = rgb.join(' '); x3d += ` `; if ( 'colors' in s.options ) { var colors = ''; for ( var j = 0 ; j < s.options.colors.length ; j++ ) { var c = s.options.colors[j]; colors += `${c.r} ${c.g} ${c.b} `; } x3d += ` `; } x3d += ` `; } x3d += ` `; if ( config.saveAsXML ) { var xml = ` ${x3d}`; var blob = new Blob( [ xml ] ); var a = document.body.appendChild( document.createElement( 'a' ) ); a.href = window.URL.createObjectURL( blob ); a.download = 'scene.xml'; a.click(); } var stylesheet = config.viewer === 'x3dom' ? `` : ``; var script = config.viewer === 'x3dom' ? `` : ` `; var html = ` ${stylesheet} ${script} ${x3d} `; var border = config.no3DBorder ? 'none' : '1px solid black'; return ``; }