Ticket #24601: graphic.html

File graphic.html, 16.8 KB (added by gh-jcamp0x2a, 21 months ago)

Example HTML plot after the changes

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@r110/build/three.min.js"></script>
29<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r110/examples/js/controls/OrbitControls.js"></script>
30           
31<script>
32
33    var scene = new THREE.Scene();
34
35    var renderer = new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: true } );
36    renderer.setPixelRatio( window.devicePixelRatio );
37    renderer.setSize( window.innerWidth, window.innerHeight );
38    renderer.setClearColor( 0xffffff, 1 );
39    document.body.appendChild( renderer.domElement );
40
41    var options = {"aspectRatio": [1.0, 1.0, 1.0], "axes": false, "axesLabels": ["x", "y", "z"], "decimals": 2, "frame": true, "projection": "perspective"};
42
43    // When animations are supported by the viewer, the value 'false'
44    // will be replaced with an option set in Python by the user
45    var animate = false; // options.animate;
46
47    var b = [{"x":0.0, "y":0.0, "z":0.0}, {"x":1.0, "y":1.0, "z":1.0}]; // bounds
48
49    if ( b[0].x === b[1].x ) {
50        b[0].x -= 1;
51        b[1].x += 1;
52    }
53    if ( b[0].y === b[1].y ) {
54        b[0].y -= 1;
55        b[1].y += 1;
56    }
57    if ( b[0].z === b[1].z ) {
58        b[0].z -= 1;
59        b[1].z += 1;
60    }
61
62    var rRange = Math.sqrt( Math.pow( b[1].x - b[0].x, 2 )
63                            + Math.pow( b[1].y - b[0].y, 2 ) );
64    var xRange = b[1].x - b[0].x;
65    var yRange = b[1].y - b[0].y;
66    var zRange = b[1].z - b[0].z;
67
68    var ar = options.aspectRatio;
69    var a = [ ar[0], ar[1], ar[2] ]; // aspect multipliers
70    var autoAspect = 2.5;
71    if ( zRange > autoAspect * rRange && a[2] === 1 ) a[2] = autoAspect * rRange / zRange;
72
73    // Distance from (xMid,yMid,zMid) to any corner of the bounding box, after applying aspectRatio
74    var midToCorner = Math.sqrt( a[0]*a[0]*xRange*xRange + a[1]*a[1]*yRange*yRange + a[2]*a[2]*zRange*zRange ) / 2;
75
76    var xMid = ( b[0].x + b[1].x ) / 2;
77    var yMid = ( b[0].y + b[1].y ) / 2;
78    var zMid = ( b[0].z + b[1].z ) / 2;
79
80    var box = new THREE.Geometry();
81    box.vertices.push( new THREE.Vector3( a[0]*b[0].x, a[1]*b[0].y, a[2]*b[0].z ) );
82    box.vertices.push( new THREE.Vector3( a[0]*b[1].x, a[1]*b[1].y, a[2]*b[1].z ) );
83    var boxMesh = new THREE.Line( box );
84    if ( options.frame ) scene.add( new THREE.BoxHelper( boxMesh, 'black' ) );
85
86    if ( options.axesLabels ) {
87
88        var d = options.decimals; // decimals
89        var offsetRatio = 0.1;
90        var al = options.axesLabels;
91
92        var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
93        var xm = xMid.toFixed(d);
94        if ( /^-0.?0*$/.test(xm) ) xm = xm.substr(1);
95        addLabel( al[0] + '=' + xm, a[0]*xMid, a[1]*b[1].y+offset, a[2]*b[0].z );
96        addLabel( ( b[0].x ).toFixed(d), a[0]*b[0].x, a[1]*b[1].y+offset, a[2]*b[0].z );
97        addLabel( ( b[1].x ).toFixed(d), a[0]*b[1].x, a[1]*b[1].y+offset, a[2]*b[0].z );
98
99        var offset = offsetRatio * a[0]*( b[1].x - b[0].x );
100        var ym = yMid.toFixed(d);
101        if ( /^-0.?0*$/.test(ym) ) ym = ym.substr(1);
102        addLabel( al[1] + '=' + ym, a[0]*b[1].x+offset, a[1]*yMid, a[2]*b[0].z );
103        addLabel( ( b[0].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[0].y, a[2]*b[0].z );
104        addLabel( ( b[1].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[1].y, a[2]*b[0].z );
105
106        var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
107        var zm = zMid.toFixed(d);
108        if ( /^-0.?0*$/.test(zm) ) zm = zm.substr(1);
109        addLabel( al[2] + '=' + zm, a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*zMid );
110        addLabel( ( b[0].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[0].z );
111        addLabel( ( b[1].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[1].z );
112
113    }
114
115    function addLabel( text, x, y, z, color='black', fontsize=14  ) {
116
117        var canvas = document.createElement( 'canvas' );
118        var pixelRatio = Math.round( window.devicePixelRatio );
119        canvas.width = 128 * pixelRatio;
120        canvas.height = 32 * pixelRatio; // powers of two
121        canvas.style.width = '128px';
122        canvas.style.height = '32px';
123
124        var context = canvas.getContext( '2d' );
125        context.scale( pixelRatio, pixelRatio );
126        context.fillStyle = color;
127        context.font = fontsize + 'px monospace';
128        context.textAlign = 'center';
129        context.textBaseline = 'middle';
130        context.fillText( text, canvas.width/2/pixelRatio, canvas.height/2/pixelRatio );
131
132        var texture = new THREE.Texture( canvas );
133        texture.needsUpdate = true;
134
135        var sprite = new THREE.Sprite( new THREE.SpriteMaterial( { map: texture } ) );
136        sprite.position.set( x, y, z );
137
138        // Set the initial scale based on plot size to accomodate orthographic projection.
139        // For other projections, the scale will get reset each frame based on camera distance.
140        var scale = midToCorner/2;
141        sprite.scale.set( scale, scale*.25, 1 ); // ratio of canvas width to height
142
143        scene.add( sprite );
144
145    }
146
147    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 ) ) );
148
149    var camera = createCamera();
150    camera.up.set( 0, 0, 1 );
151    camera.position.set( a[0]*(xMid+xRange), a[1]*(yMid+yRange), a[2]*(zMid+zRange) );
152
153    function createCamera() {
154
155        var aspect = window.innerWidth / window.innerHeight;
156
157        if ( options.projection === 'orthographic' ) {
158            var camera = new THREE.OrthographicCamera( -1, 1, 1, -1, -1000, 1000 );
159            updateCameraAspect( camera, aspect );
160            return camera;
161        }
162
163        return new THREE.PerspectiveCamera( 45, aspect, 0.1, 1000 );
164
165    }
166
167    function updateCameraAspect( camera, aspect ) {
168
169        if ( camera.isPerspectiveCamera ) {
170            camera.aspect = aspect;
171        } else if ( camera.isOrthographicCamera ) {
172            // Fit the camera frustum to the bounding box's diagonal so that the entire plot fits
173            // within at the default zoom level and camera position.
174            if ( aspect > 1 ) { // Wide window
175                camera.top = midToCorner;
176                camera.right = midToCorner * aspect;
177            } else { // Tall or square window
178                camera.top = midToCorner / aspect;
179                camera.right = midToCorner;
180            }
181            camera.bottom = -camera.top;
182            camera.left = -camera.right;
183        }
184
185        camera.updateProjectionMatrix();
186
187    }
188
189    var lights = [{"x":-5, "y":3, "z":0, "color":"#7f7f7f", "parent":"camera"}];
190    for ( var i=0 ; i < lights.length ; i++ ) {
191        var light = new THREE.DirectionalLight( lights[i].color, 1 );
192        light.position.set( a[0]*lights[i].x, a[1]*lights[i].y, a[2]*lights[i].z );
193        if ( lights[i].parent === 'camera' ) {
194            light.target.position.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
195            scene.add( light.target );
196            camera.add( light );
197        } else scene.add( light );
198    }
199    scene.add( camera );
200
201    var ambient = {"color":"#7f7f7f"};
202    scene.add( new THREE.AmbientLight( ambient.color, 1 ) );
203
204    var controls = new THREE.OrbitControls( camera, renderer.domElement );
205    controls.target.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
206    controls.addEventListener( 'change', function() { if ( !animate ) render(); } );
207
208    window.addEventListener( 'resize', function() {
209       
210        renderer.setSize( window.innerWidth, window.innerHeight );
211        updateCameraAspect( camera, window.innerWidth / window.innerHeight );
212        if ( !animate ) render();
213       
214    } );
215
216    var texts = [];
217    for ( var i=0 ; i < texts.length ; i++ )
218        addLabel( texts[i].text, a[0]*texts[i].x, a[1]*texts[i].y, a[2]*texts[i].z, texts[i].color );
219
220    var points = [];
221    for ( var i=0 ; i < points.length ; i++ ) addPoint( points[i] );
222
223    function addPoint( json ) {
224
225        var geometry = new THREE.Geometry();
226        var v = json.point;
227        geometry.vertices.push( new THREE.Vector3( a[0]*v[0], a[1]*v[1], a[2]*v[2] ) );
228
229        var canvas = document.createElement( 'canvas' );
230        canvas.width = 128;
231        canvas.height = 128;
232
233        var context = canvas.getContext( '2d' );
234        context.arc( 64, 64, 64, 0, 2 * Math.PI );
235        context.fillStyle = json.color;
236        context.fill();
237
238        var texture = new THREE.Texture( canvas );
239        texture.needsUpdate = true;
240
241        var transparent = json.opacity < 1 ? true : false;
242        var size = camera.isOrthographicCamera ? json.size : json.size/100;
243        var material = new THREE.PointsMaterial( { size: size, map: texture,
244                                                   transparent: transparent, opacity: json.opacity,
245                                                   alphaTest: .1 } );
246
247        var c = new THREE.Vector3();
248        geometry.computeBoundingBox();
249        geometry.boundingBox.getCenter( c );
250        geometry.translate( -c.x, -c.y, -c.z );
251
252        var mesh = new THREE.Points( geometry, material );
253        mesh.position.set( c.x, c.y, c.z );
254        scene.add( mesh );
255
256    }
257
258    var lines = [{"points": [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], "color": "#6666ff", "opacity": 1.0, "linewidth": 1.0}];
259    for ( var i=0 ; i < lines.length ; i++ ) addLine( lines[i] );
260
261    function addLine( json ) {
262
263        var geometry = new THREE.Geometry();
264        for ( var i=0 ; i < json.points.length ; i++ ) {
265            var v = json.points[i];
266            geometry.vertices.push( new THREE.Vector3( a[0]*v[0], a[1]*v[1], a[2]*v[2] ) );
267        }
268
269        var transparent = json.opacity < 1 ? true : false;
270        var material = new THREE.LineBasicMaterial( { color: json.color, linewidth: json.linewidth,
271                                                      transparent: transparent, opacity: json.opacity } );
272
273        var c = new THREE.Vector3();
274        geometry.computeBoundingBox();
275        geometry.boundingBox.getCenter( c );
276        geometry.translate( -c.x, -c.y, -c.z );
277
278        var mesh = new THREE.Line( geometry, material );
279        mesh.position.set( c.x, c.y, c.z );
280        scene.add( mesh );
281
282    }
283
284    var surfaces = [];
285    for ( var i=0 ; i < surfaces.length ; i++ ) addSurface( surfaces[i] );
286
287    function addSurface( json ) {
288
289        var useFaceColors = 'faceColors' in json ? true : false;
290
291        var geometry = new THREE.Geometry();
292        for ( var i=0 ; i < json.vertices.length ; i++ ) {
293            var v = json.vertices[i];
294            geometry.vertices.push( new THREE.Vector3( a[0]*v.x, a[1]*v.y, a[2]*v.z ) );
295        }
296        for ( var i=0 ; i < json.faces.length ; i++ ) {
297            var f = json.faces[i];
298            for ( var j=0 ; j < f.length - 2 ; j++ ) {
299                var face = new THREE.Face3( f[0], f[j+1], f[j+2] );
300                if ( useFaceColors ) face.color.set( json.faceColors[i] );
301                geometry.faces.push( face );
302            }
303        }
304        geometry.computeVertexNormals();
305
306        var side = json.singleSide ? THREE.FrontSide : THREE.DoubleSide;
307        var transparent = json.opacity < 1 ? true : false;
308
309        var material = new THREE.MeshPhongMaterial( { side: side,
310                                     color: useFaceColors ? 'white' : json.color,
311                                     vertexColors: useFaceColors ? THREE.FaceColors : THREE.NoColors,
312                                     transparent: transparent, opacity: json.opacity,
313                                     shininess: 20, flatShading: json.useFlatShading } );
314
315        var c = new THREE.Vector3();
316        geometry.computeBoundingBox();
317        geometry.boundingBox.getCenter( c );
318        geometry.translate( -c.x, -c.y, -c.z );
319
320        var mesh = new THREE.Mesh( geometry, material );
321        mesh.position.set( c.x, c.y, c.z );
322        if ( transparent && json.renderOrder ) mesh.renderOrder = json.renderOrder;
323        scene.add( mesh );
324
325        if ( json.showMeshGrid ) {
326
327            var geometry = new THREE.Geometry();
328
329            for ( var i=0 ; i < json.faces.length ; i++ ) {
330                var f = json.faces[i];
331                for ( var j=0 ; j < f.length ; j++ ) {
332                    var k = j === f.length-1 ? 0 : j+1;
333                    var v1 = json.vertices[f[j]];
334                    var v2 = json.vertices[f[k]];
335                    // vertices in opposite directions on neighboring faces
336                    var nudge = f[j] < f[k] ? .0005*zRange : -.0005*zRange;
337                    geometry.vertices.push( new THREE.Vector3( a[0]*v1.x, a[1]*v1.y, a[2]*(v1.z+nudge) ) );
338                    geometry.vertices.push( new THREE.Vector3( a[0]*v2.x, a[1]*v2.y, a[2]*(v2.z+nudge) ) );
339                }
340            }
341
342            var material = new THREE.LineBasicMaterial( { color: 'black', linewidth: 1 } );
343
344            var c = new THREE.Vector3();
345            geometry.computeBoundingBox();
346            geometry.boundingBox.getCenter( c );
347            geometry.translate( -c.x, -c.y, -c.z );
348
349            var mesh = new THREE.LineSegments( geometry, material );
350            mesh.position.set( c.x, c.y, c.z );
351            scene.add( mesh );
352
353        }
354
355    }
356
357    var scratch = new THREE.Vector3();
358
359    function render() {
360
361        if ( animate ) requestAnimationFrame( render );
362        renderer.render( scene, camera );
363
364        // Resize text based on distance from camera.
365        // Not neccessary for orthographic due to the nature of the projection (preserves sizes).
366        if ( !camera.isOrthographicCamera ) {
367            for ( var i=0 ; i < scene.children.length ; i++ ) {
368                if ( scene.children[i].type === 'Sprite' ) {
369                    var sprite = scene.children[i];
370                    var adjust = scratch.addVectors( sprite.position, scene.position )
371                                    .sub( camera.position ).length() / 5;
372                    sprite.scale.set( adjust, .25*adjust, 1 ); // ratio of canvas width to height
373                }
374            }
375        }
376    }
377   
378    render();
379    controls.update();
380    if ( !animate ) render();
381
382
383    // menu functions
384
385    function toggleMenu() {
386
387        var m = document.getElementById( 'menu-content' );
388        if ( m.style.display === 'block' ) m.style.display = 'none'
389        else m.style.display = 'block';
390
391    }
392
393
394    function saveAsPNG() {
395
396        var a = document.body.appendChild( document.createElement( 'a' ) );
397        a.href = renderer.domElement.toDataURL( 'image/png' );
398        a.download = 'screenshot';
399        a.click();
400
401    }
402
403    function saveAsHTML() {
404
405        toggleMenu(); // otherwise visible in output
406        event.stopPropagation();
407
408        var blob = new Blob( [ '<!DOCTYPE html>\n' + document.documentElement.outerHTML ] );
409        var a = document.body.appendChild( document.createElement( 'a' ) );
410        a.href = window.URL.createObjectURL( blob );
411        a.download = 'graphic.html';
412        a.click();
413
414    }
415
416    function getViewpoint() {
417
418        function roundTo( x, n ) { return +x.toFixed(n); }
419
420        var v = camera.quaternion.inverse();
421        var r = Math.sqrt( v.x*v.x + v.y*v.y + v.z*v.z );
422        var axis = [ roundTo( v.x / r, 4 ), roundTo( v.y / r, 4 ), roundTo( v.z / r, 4 ) ];
423        var angle = roundTo( 2 * Math.atan2( r, v.w ) * 180 / Math.PI, 2 );
424
425        var textArea = document.createElement( 'textarea' );
426        textArea.textContent = JSON.stringify( axis ) + ',' + angle;
427        textArea.style.csstext = 'position: absolute; top: -100%';
428        document.body.append( textArea );
429        textArea.select();
430        document.execCommand( 'copy' );
431
432        var m = document.getElementById( 'menu-message' );
433        m.innerHTML = 'Viewpoint copied to clipboard';
434        m.style.display = 'block';
435        setTimeout( function() { m.style.display = 'none'; }, 2000 );
436
437    }
438
439</script><canvas width="2465" height="1000" style="width: 986px; height: 400px;" tabindex="0"></canvas>
440
441<div id="menu-container" onclick="toggleMenu()">
442<div id="menu-message"></div>
443<div id="menu-content" style="display: none;">
444<div onclick="saveAsPNG()">Save as PNG</div>
445<div onclick="saveAsHTML()">Save as HTML</div>
446<div onclick="getViewpoint()">Get Viewpoint</div>
447<div>Close Menu</div>
448</div></div>
449
450
451
452</body></html>