Ticket #30226: mathjax2canvas_threejs.html

File mathjax2canvas_threejs.html, 20.4 KB (added by Joshua Campbell, 2 years ago)
Line 
1<!DOCTYPE html>
2<html><head>
3<title></title>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
6<style>
7
8    body { margin: 0px; overflow: hidden; }
9
10    #menu-container { position: absolute; bottom: 30px; right: 40px; cursor: default; }
11
12    #menu-message { position: absolute; bottom: 0px; right: 0px; white-space: nowrap;
13                    display: none; background-color: #F5F5F5; padding: 10px; }
14
15    #menu-content { position: absolute; bottom: 0px; right: 0px;
16                    display: none; background-color: #F5F5F5; border-bottom: 1px solid black;
17                    border-right: 1px solid black; border-left: 1px solid black; }
18
19    #menu-content div { border-top: 1px solid black; padding: 10px; white-space: nowrap; }
20
21    #menu-content div:hover { background-color: #FEFEFE;; }
22
23</style>
24</head>
25
26<body>
27
28<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r117/build/three.min.js"></script>
29<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r117/examples/js/controls/OrbitControls.js"></script>
30<script type="text/javascript">
31    window.MathJax = {
32        SVG: {
33            useGlobalCache: false
34        }
35    };
36</script>
37<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.4/MathJax.js?config=TeX-AMS_SVG"></script>
38           
39<script>
40
41    var scene = new THREE.Scene();
42
43    var renderer = new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: true } );
44    renderer.setPixelRatio( window.devicePixelRatio );
45    renderer.setSize( window.innerWidth, window.innerHeight );
46    renderer.setClearColor( 0xffffff, 1 );
47    document.body.appendChild( renderer.domElement );
48
49    var options = {"aspectRatio": [1.0, 1.0, 1.0], "axes": false, "axesLabels": ["\\alpha", "\\beta", "\\gamma"], "decimals": 2, "frame": true, "projection": "perspective", "viewpoint": false};
50
51    // When animations are supported by the viewer, the value 'false'
52    // will be replaced with an option set in Python by the user
53    var animate = false; // options.animate;
54
55    var b = [{"x":-1.0, "y":-1.0, "z":-1.0}, {"x":1.0, "y":1.0, "z":1.0}]; // bounds
56
57    if ( b[0].x === b[1].x ) {
58        b[0].x -= 1;
59        b[1].x += 1;
60    }
61    if ( b[0].y === b[1].y ) {
62        b[0].y -= 1;
63        b[1].y += 1;
64    }
65    if ( b[0].z === b[1].z ) {
66        b[0].z -= 1;
67        b[1].z += 1;
68    }
69
70    var rRange = Math.sqrt( Math.pow( b[1].x - b[0].x, 2 )
71                            + Math.pow( b[1].y - b[0].y, 2 ) );
72    var xRange = b[1].x - b[0].x;
73    var yRange = b[1].y - b[0].y;
74    var zRange = b[1].z - b[0].z;
75
76    var ar = options.aspectRatio;
77    var a = [ ar[0], ar[1], ar[2] ]; // aspect multipliers
78    var autoAspect = 2.5;
79    if ( zRange > autoAspect * rRange && a[2] === 1 ) a[2] = autoAspect * rRange / zRange;
80
81    // Distance from (xMid,yMid,zMid) to any corner of the bounding box, after applying aspectRatio
82    var midToCorner = Math.sqrt( a[0]*a[0]*xRange*xRange + a[1]*a[1]*yRange*yRange + a[2]*a[2]*zRange*zRange ) / 2;
83
84    var xMid = ( b[0].x + b[1].x ) / 2;
85    var yMid = ( b[0].y + b[1].y ) / 2;
86    var zMid = ( b[0].z + b[1].z ) / 2;
87
88    var box = new THREE.Geometry();
89    box.vertices.push( new THREE.Vector3( a[0]*b[0].x, a[1]*b[0].y, a[2]*b[0].z ) );
90    box.vertices.push( new THREE.Vector3( a[0]*b[1].x, a[1]*b[1].y, a[2]*b[1].z ) );
91    var boxMesh = new THREE.Line( box );
92    if ( options.frame ) scene.add( new THREE.BoxHelper( boxMesh, 'black' ) );
93
94    if ( options.axesLabels ) {
95
96        var d = options.decimals; // decimals
97        var offsetRatio = 0.1;
98        var al = options.axesLabels;
99
100        var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
101        var xm = xMid.toFixed(d);
102        if ( /^-0.?0*$/.test(xm) ) xm = xm.substr(1);
103        addLabel( al[0] + '=' + xm, a[0]*xMid, a[1]*b[1].y+offset, a[2]*b[0].z );
104        addLabel( ( b[0].x ).toFixed(d), a[0]*b[0].x, a[1]*b[1].y+offset, a[2]*b[0].z );
105        addLabel( ( b[1].x ).toFixed(d), a[0]*b[1].x, a[1]*b[1].y+offset, a[2]*b[0].z );
106
107        var offset = offsetRatio * a[0]*( b[1].x - b[0].x );
108        var ym = yMid.toFixed(d);
109        if ( /^-0.?0*$/.test(ym) ) ym = ym.substr(1);
110        addLabel( al[1] + '=' + ym, a[0]*b[1].x+offset, a[1]*yMid, a[2]*b[0].z );
111        addLabel( ( b[0].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[0].y, a[2]*b[0].z );
112        addLabel( ( b[1].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[1].y, a[2]*b[0].z );
113
114        var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
115        var zm = zMid.toFixed(d);
116        if ( /^-0.?0*$/.test(zm) ) zm = zm.substr(1);
117        addLabel( al[2] + '=' + zm, a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*zMid );
118        addLabel( ( b[0].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[0].z );
119        addLabel( ( b[1].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[1].z );
120
121    }
122
123    function addLabel( text, x, y, z, color='black', fontsize=14 ) {
124   
125        // TODO: how does user specify whether to interpret text as LaTeX?
126        //       parameter? try to auto-detect? make everything LaTeX by default?
127        text = '$$' + text + '$$';
128        addLatexLabel( text, x, y, z, color, fontsize );
129
130        /*
131        var canvas = document.createElement( 'canvas' );
132        var context = canvas.getContext( '2d' );
133        var pixelRatio = Math.round( window.devicePixelRatio );
134
135        context.font = fontsize + 'px monospace';
136        var width = context.measureText( text ).width;
137        var height = fontsize;
138
139        // The dimensions of the canvas's underlying image data need to be powers
140        // of two in order for the resulting texture to support mipmapping.
141        canvas.width = THREE.MathUtils.ceilPowerOfTwo( width * pixelRatio );
142        canvas.height = THREE.MathUtils.ceilPowerOfTwo( height * pixelRatio );
143
144        // Re-compute the unscaled dimensions after the power of two conversion.
145        width = canvas.width / pixelRatio;
146        height = canvas.height / pixelRatio;
147
148        canvas.style.width = width + 'px';
149        canvas.style.height = height + 'px';
150
151        context.scale( pixelRatio, pixelRatio );
152        context.fillStyle = color;
153        context.font = fontsize + 'px monospace'; // Must be set again after measureText.
154        context.textAlign = 'center';
155        context.textBaseline = 'middle';
156        context.fillText( text, width/2, height/2 );
157
158        var texture = new THREE.Texture( canvas );
159        texture.needsUpdate = true;
160
161        var materialOptions = { map: texture, sizeAttenuation: false };
162        var sprite = new THREE.Sprite( new THREE.SpriteMaterial( materialOptions ) );
163        sprite.position.set( x, y, z );
164
165        // Scaling factor, chosen somewhat arbitrarily so that the size of the text
166        // is consistent with previously generated plots.
167        var scale = 1/625;
168        if ( options.projection === 'orthographic' ) {
169            scale = midToCorner/256; // Needs to scale along with the plot itself.
170        }
171        sprite.scale.set( scale * width, scale * height, 1 );
172
173        scene.add( sprite );
174        */
175
176    }
177   
178    function addLatexLabel( text, x, y, z, color='black', fontsize=14 ) {
179   
180        console.log(text);
181       
182        // https://stackoverflow.com/questions/34924033/convert-latex-mathml-to-svg-or-image-with-mathjax-or-similar/35430069#35430069
183       
184        var div = document.createElement( 'div' );
185        div.appendChild( document.createTextNode( text ) ); // TODO: fontsize and color
186        document.body.appendChild( div );
187       
188        MathJax.Hub.Queue( ['Typeset', MathJax.Hub, div] );
189        MathJax.Hub.Queue( typesetComplete );
190       
191        function typesetComplete() {
192       
193            var svg = div.getElementsByTagName( 'svg' )[0];
194            var svgSize = svg.getBoundingClientRect();
195            var svgData = new XMLSerializer().serializeToString( svg );
196           
197            document.body.removeChild( div );
198           
199            var img = document.createElement( 'img' );
200            var src = window.btoa( unescape( encodeURIComponent( svgData ) ) );
201            img.setAttribute( 'src', 'data:image/svg+xml;base64,' + src );
202            img.onload = function() { imageLoaded( img, svgSize ); };
203               
204        }
205       
206        function imageLoaded( img, svgSize ) {
207           
208            var width = svgSize.width;
209            var height = svgSize.height;
210            var pixelRatio = Math.round( window.devicePixelRatio );
211           
212            var canvas = document.createElement( 'canvas' );
213            canvas.width = THREE.MathUtils.ceilPowerOfTwo( width * pixelRatio );
214            canvas.height = THREE.MathUtils.ceilPowerOfTwo( height * pixelRatio );
215           
216            width = canvas.width / pixelRatio;
217            height = canvas.height / pixelRatio;
218           
219            canvas.style.width = width + 'px';
220            canvas.style.height = height + 'px';
221           
222            var context = canvas.getContext( '2d' );
223            context.scale( pixelRatio, pixelRatio );
224            context.drawImage( img, 0, 0 );
225           
226            var texture = new THREE.Texture( canvas );
227            texture.needsUpdate = true;
228
229            var materialOptions = { map: texture, sizeAttenuation: false };
230            var sprite = new THREE.Sprite( new THREE.SpriteMaterial( materialOptions ) );
231            sprite.position.set( x, y, z );
232
233            // Scaling factor, chosen somewhat arbitrarily so that the size of the text
234            // is consistent with previously generated plots.
235            var scale = 1/625;
236            if ( options.projection === 'orthographic' ) {
237                scale = midToCorner/256; // Needs to scale along with the plot itself.
238            }
239            sprite.scale.set( scale * width, scale * height, 1 );
240
241            scene.add( sprite );
242           
243            render();
244           
245        }
246       
247    }
248
249    if ( options.axes ) scene.add( new THREE.AxesHelper( Math.min( a[0]*b[1].x, a[1]*b[1].y, a[2]*b[1].z ) ) );
250
251    var camera = createCamera();
252    camera.up.set( 0, 0, 1 );
253    camera.position.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
254
255    var offset = new THREE.Vector3( a[0]*xRange, a[1]*yRange, a[2]*zRange );
256
257    if ( options.viewpoint ) {
258
259        var aa = options.viewpoint;
260        var axis = new THREE.Vector3( aa[0][0], aa[0][1], aa[0][2] ).normalize();
261        var angle = aa[1] * Math.PI / 180;
262        var q = new THREE.Quaternion().setFromAxisAngle( axis, angle ).inverse();
263
264        offset.set( 0, 0, offset.length() );
265        offset.applyQuaternion( q );
266
267    }
268
269    camera.position.add( offset );
270
271    function createCamera() {
272
273        var aspect = window.innerWidth / window.innerHeight;
274
275        if ( options.projection === 'orthographic' ) {
276            var camera = new THREE.OrthographicCamera( -1, 1, 1, -1, -1000, 1000 );
277            updateCameraAspect( camera, aspect );
278            return camera;
279        }
280
281        return new THREE.PerspectiveCamera( 45, aspect, 0.1, 1000 );
282
283    }
284
285    function updateCameraAspect( camera, aspect ) {
286
287        if ( camera.isPerspectiveCamera ) {
288            camera.aspect = aspect;
289        } else if ( camera.isOrthographicCamera ) {
290            // Fit the camera frustum to the bounding box's diagonal so that the entire plot fits
291            // within at the default zoom level and camera position.
292            if ( aspect > 1 ) { // Wide window
293                camera.top = midToCorner;
294                camera.right = midToCorner * aspect;
295            } else { // Tall or square window
296                camera.top = midToCorner / aspect;
297                camera.right = midToCorner;
298            }
299            camera.bottom = -camera.top;
300            camera.left = -camera.right;
301        }
302
303        camera.updateProjectionMatrix();
304
305    }
306
307    var lights = [{"x":-5, "y":3, "z":0, "color":"#7f7f7f", "parent":"camera"}];
308    for ( var i=0 ; i < lights.length ; i++ ) {
309        var light = new THREE.DirectionalLight( lights[i].color, 1 );
310        light.position.set( a[0]*lights[i].x, a[1]*lights[i].y, a[2]*lights[i].z );
311        if ( lights[i].parent === 'camera' ) {
312            light.target.position.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
313            scene.add( light.target );
314            camera.add( light );
315        } else scene.add( light );
316    }
317    scene.add( camera );
318
319    var ambient = {"color":"#7f7f7f"};
320    scene.add( new THREE.AmbientLight( ambient.color, 1 ) );
321
322    var controls = new THREE.OrbitControls( camera, renderer.domElement );
323    controls.target.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
324    controls.addEventListener( 'change', function() { if ( !animate ) render(); } );
325
326    window.addEventListener( 'resize', function() {
327
328        renderer.setSize( window.innerWidth, window.innerHeight );
329        updateCameraAspect( camera, window.innerWidth / window.innerHeight );
330        if ( !animate ) render();
331
332    } );
333
334    var texts = [{"text": "x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}", "x": 0.0, "y": 0.0, "z": 0.0, "color": "#000000"}];
335    for ( var i=0 ; i < texts.length ; i++ )
336        addLabel( texts[i].text, a[0]*texts[i].x, a[1]*texts[i].y, a[2]*texts[i].z, texts[i].color );
337
338    var points = [];
339    for ( var i=0 ; i < points.length ; i++ ) addPoint( points[i] );
340
341    function addPoint( json ) {
342
343        var geometry = new THREE.Geometry();
344        var v = json.point;
345        geometry.vertices.push( new THREE.Vector3( a[0]*v[0], a[1]*v[1], a[2]*v[2] ) );
346
347        var canvas = document.createElement( 'canvas' );
348        canvas.width = 128;
349        canvas.height = 128;
350
351        var context = canvas.getContext( '2d' );
352        context.arc( 64, 64, 64, 0, 2 * Math.PI );
353        context.fillStyle = json.color;
354        context.fill();
355
356        var texture = new THREE.Texture( canvas );
357        texture.needsUpdate = true;
358
359        var transparent = json.opacity < 1 ? true : false;
360        var size = camera.isOrthographicCamera ? json.size : json.size/100;
361        var material = new THREE.PointsMaterial( { size: size, map: texture,
362                                                   transparent: transparent, opacity: json.opacity,
363                                                   alphaTest: .1 } );
364
365        var c = new THREE.Vector3();
366        geometry.computeBoundingBox();
367        geometry.boundingBox.getCenter( c );
368        geometry.translate( -c.x, -c.y, -c.z );
369
370        var mesh = new THREE.Points( geometry, material );
371        mesh.position.set( c.x, c.y, c.z );
372        scene.add( mesh );
373
374    }
375
376    var lines = [];
377    for ( var i=0 ; i < lines.length ; i++ ) addLine( lines[i] );
378
379    function addLine( json ) {
380
381        var geometry = new THREE.Geometry();
382        for ( var i=0 ; i < json.points.length ; i++ ) {
383            var v = json.points[i];
384            geometry.vertices.push( new THREE.Vector3( a[0]*v[0], a[1]*v[1], a[2]*v[2] ) );
385        }
386
387        var transparent = json.opacity < 1 ? true : false;
388        var material = new THREE.LineBasicMaterial( { color: json.color, linewidth: json.linewidth,
389                                                      transparent: transparent, opacity: json.opacity } );
390
391        var c = new THREE.Vector3();
392        geometry.computeBoundingBox();
393        geometry.boundingBox.getCenter( c );
394        geometry.translate( -c.x, -c.y, -c.z );
395
396        var mesh = new THREE.Line( geometry, material );
397        mesh.position.set( c.x, c.y, c.z );
398        scene.add( mesh );
399
400    }
401
402    var surfaces = [];
403    for ( var i=0 ; i < surfaces.length ; i++ ) addSurface( surfaces[i] );
404
405    function addSurface( json ) {
406
407        var useFaceColors = 'faceColors' in json ? true : false;
408
409        var geometry = new THREE.Geometry();
410        for ( var i=0 ; i < json.vertices.length ; i++ ) {
411            var v = json.vertices[i];
412            geometry.vertices.push( new THREE.Vector3( a[0]*v.x, a[1]*v.y, a[2]*v.z ) );
413        }
414        for ( var i=0 ; i < json.faces.length ; i++ ) {
415            var f = json.faces[i];
416            for ( var j=0 ; j < f.length - 2 ; j++ ) {
417                var face = new THREE.Face3( f[0], f[j+1], f[j+2] );
418                if ( useFaceColors ) face.color.set( json.faceColors[i] );
419                geometry.faces.push( face );
420            }
421        }
422        geometry.computeVertexNormals();
423
424        var side = json.singleSide ? THREE.FrontSide : THREE.DoubleSide;
425        var transparent = json.opacity < 1 ? true : false;
426        var flatShading = json.useFlatShading ? json.useFlatShading : false;
427
428        var material = new THREE.MeshPhongMaterial( { side: side,
429                                     color: useFaceColors ? 'white' : json.color,
430                                     vertexColors: useFaceColors ? THREE.FaceColors : THREE.NoColors,
431                                     transparent: transparent, opacity: json.opacity,
432                                     shininess: 20, flatShading: flatShading } );
433
434        var c = new THREE.Vector3();
435        geometry.computeBoundingBox();
436        geometry.boundingBox.getCenter( c );
437        geometry.translate( -c.x, -c.y, -c.z );
438
439        var mesh = new THREE.Mesh( geometry, material );
440        mesh.position.set( c.x, c.y, c.z );
441        if ( transparent && json.renderOrder ) mesh.renderOrder = json.renderOrder;
442        scene.add( mesh );
443
444        if ( json.showMeshGrid ) {
445
446            var geometry = new THREE.Geometry();
447
448            for ( var i=0 ; i < json.faces.length ; i++ ) {
449                var f = json.faces[i];
450                for ( var j=0 ; j < f.length ; j++ ) {
451                    var k = j === f.length-1 ? 0 : j+1;
452                    var v1 = json.vertices[f[j]];
453                    var v2 = json.vertices[f[k]];
454                    // vertices in opposite directions on neighboring faces
455                    var nudge = f[j] < f[k] ? .0005*zRange : -.0005*zRange;
456                    geometry.vertices.push( new THREE.Vector3( a[0]*v1.x, a[1]*v1.y, a[2]*(v1.z+nudge) ) );
457                    geometry.vertices.push( new THREE.Vector3( a[0]*v2.x, a[1]*v2.y, a[2]*(v2.z+nudge) ) );
458                }
459            }
460
461            var material = new THREE.LineBasicMaterial( { color: 'black', linewidth: 1 } );
462
463            var c = new THREE.Vector3();
464            geometry.computeBoundingBox();
465            geometry.boundingBox.getCenter( c );
466            geometry.translate( -c.x, -c.y, -c.z );
467
468            var mesh = new THREE.LineSegments( geometry, material );
469            mesh.position.set( c.x, c.y, c.z );
470            scene.add( mesh );
471
472        }
473
474    }
475
476    function render() {
477
478        if ( animate ) requestAnimationFrame( render );
479        renderer.render( scene, camera );
480
481    }
482
483    render();
484    controls.update();
485    if ( !animate ) render();
486
487
488    // menu functions
489
490    function toggleMenu() {
491
492        var m = document.getElementById( 'menu-content' );
493        if ( m.style.display === 'block' ) m.style.display = 'none'
494        else m.style.display = 'block';
495
496    }
497
498
499    function saveAsPNG() {
500
501        var a = document.body.appendChild( document.createElement( 'a' ) );
502        a.href = renderer.domElement.toDataURL( 'image/png' );
503        a.download = 'screenshot';
504        a.click();
505
506    }
507
508    function saveAsHTML() {
509
510        toggleMenu(); // otherwise visible in output
511        event.stopPropagation();
512
513        var blob = new Blob( [ '<!DOCTYPE html>\n' + document.documentElement.outerHTML ] );
514        var a = document.body.appendChild( document.createElement( 'a' ) );
515        a.href = window.URL.createObjectURL( blob );
516        a.download = 'graphic.html';
517        a.click();
518
519    }
520
521    function getViewpoint() {
522
523        function roundTo( x, n ) { return +x.toFixed(n); }
524
525        var v = camera.quaternion.inverse();
526        var r = Math.sqrt( v.x*v.x + v.y*v.y + v.z*v.z );
527        var axis = [ roundTo( v.x / r, 4 ), roundTo( v.y / r, 4 ), roundTo( v.z / r, 4 ) ];
528        var angle = roundTo( 2 * Math.atan2( r, v.w ) * 180 / Math.PI, 2 );
529
530        var textArea = document.createElement( 'textarea' );
531        textArea.textContent = JSON.stringify( axis ) + ',' + angle;
532        textArea.style.csstext = 'position: absolute; top: -100%';
533        document.body.append( textArea );
534        textArea.select();
535        document.execCommand( 'copy' );
536
537        var m = document.getElementById( 'menu-message' );
538        m.innerHTML = 'Viewpoint copied to clipboard';
539        m.style.display = 'block';
540        setTimeout( function() { m.style.display = 'none'; }, 2000 );
541
542    }
543
544</script>
545
546<div id="menu-container" onclick="toggleMenu()">
547<div id="menu-message"></div>
548<div id="menu-content" style="display: none;">
549<div onclick="saveAsPNG()">Save as PNG</div>
550<div onclick="saveAsHTML()">Save as HTML</div>
551<div onclick="getViewpoint()">Get Viewpoint</div>
552<div>Close Menu</div>
553</div></div>
554
555
556
557</body></html>