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 += '
' + data[i][j] + '
';
}
s += '
';
}
s += '
';
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( '