/* MATTER OS structure viewer - dependency-free canvas 3D renderer (Three.js/3Dmol-style interaction). * Works offline (no CDN). Drag = rotate, wheel = zoom, shift-drag = pan. * Overlays: bonds (graph, opacity = edge confidence), unit cell, forces, labels, trajectory slider (initial -> relaxed). * Colours encode element only (Jmol palette); evidence state is shown as text, never as styling. */ (function (global) { function MatterViewer(canvas) { this.c = canvas; this.ctx = canvas.getContext('2d'); this.rot = [[1,0,0],[0,1,0],[0,0,1]]; this.zoom = 1; this.pan = [0,0]; this.data = null; this.frame = null; this.show = {bonds: true, cell: true, labels: false, forces: false}; var self = this, drag = null; canvas.addEventListener('mousedown', function (e) { drag = {x: e.clientX, y: e.clientY, shift: e.shiftKey}; }); window.addEventListener('mouseup', function () { drag = null; }); window.addEventListener('mousemove', function (e) { if (!drag) return; var dx = e.clientX - drag.x, dy = e.clientY - drag.y; drag.x = e.clientX; drag.y = e.clientY; if (drag.shift) { self.pan[0] += dx; self.pan[1] += dy; } else { self.rotate(dy * 0.01, dx * 0.01); } self.draw(); }); canvas.addEventListener('wheel', function (e) { e.preventDefault(); self.zoom *= (e.deltaY < 0 ? 1.1 : 0.9); self.draw(); }, {passive: false}); } function mul(a, b) { var r = [[0,0,0],[0,0,0],[0,0,0]]; for (var i=0;i<3;i++) for (var j=0;j<3;j++) for (var k=0;k<3;k++) r[i][j]+=a[i][k]*b[k][j]; return r; } MatterViewer.prototype.rotate = function (ax, ay) { var cx = Math.cos(ax), sx = Math.sin(ax), cy = Math.cos(ay), sy = Math.sin(ay); var Rx = [[1,0,0],[0,cx,-sx],[0,sx,cx]], Ry = [[cy,0,sy],[0,1,0],[-sy,0,cy]]; this.rot = mul(Ry, mul(Rx, this.rot)); }; MatterViewer.prototype.load = function (data) { this.data = data; this.frame = null; var P = data.atoms.map(function (a) { return a.position; }); var c = [0,0,0]; P.forEach(function (p) { c[0]+=p[0]; c[1]+=p[1]; c[2]+=p[2]; }); this.center = c.map(function (x) { return x / Math.max(1, P.length); }); var r = 1; P.forEach(function (p) { r = Math.max(r, Math.hypot(p[0]-c[0]/P.length, p[1]-c[1]/P.length, p[2]-c[2]/P.length)); }); if (data.cell) r = Math.max(r, 0.6 * Math.hypot.apply(null, data.cell[0].map(function (_, i) { return data.cell[0][i] + data.cell[1][i] + data.cell[2][i]; }))); this.scale = 0.42 * Math.min(this.c.width, this.c.height) / (r + 1.5); this.draw(); }; MatterViewer.prototype.positions = function () { var d = this.data; if (this.frame !== null && d.trajectory) { var f = this.frame, A = d.trajectory[0], B = d.trajectory[1]; return A.map(function (p, i) { return [0,1,2].map(function (k) { return p[k] + f * (B[i][k] - p[k]); }); }); } return d.atoms.map(function (a) { return a.position; }); }; MatterViewer.prototype.project = function (p) { var q = [p[0]-this.center[0], p[1]-this.center[1], p[2]-this.center[2]], R = this.rot; var x = R[0][0]*q[0]+R[0][1]*q[1]+R[0][2]*q[2], y = R[1][0]*q[0]+R[1][1]*q[1]+R[1][2]*q[2], z = R[2][0]*q[0]+R[2][1]*q[1]+R[2][2]*q[2]; var s = this.scale * this.zoom; return [this.c.width/2 + this.pan[0] + x*s, this.c.height/2 + this.pan[1] - y*s, z]; }; MatterViewer.prototype.draw = function () { var ctx = this.ctx, d = this.data, self = this; if (!d) return; ctx.fillStyle = '#10141a'; ctx.fillRect(0, 0, this.c.width, this.c.height); var P = this.positions(), S = P.map(function (p) { return self.project(p); }); if (this.show.cell && d.cell) { var o = [0,0,0], C = d.cell, corners = []; for (var i=0;i<2;i++) for (var j=0;j<2;j++) for (var k=0;k<2;k++) corners.push([0,1,2].map(function (m) { return i*C[0][m]+j*C[1][m]+k*C[2][m]; })); var edges = [[0,1],[0,2],[0,4],[1,3],[1,5],[2,3],[2,6],[3,7],[4,5],[4,6],[5,7],[6,7]]; ctx.strokeStyle = '#5a6b7d'; ctx.lineWidth = 1; ctx.setLineDash([4,3]); edges.forEach(function (e) { var a = self.project(corners[e[0]]), b = self.project(corners[e[1]]); ctx.beginPath(); ctx.moveTo(a[0],a[1]); ctx.lineTo(b[0],b[1]); ctx.stroke(); }); ctx.setLineDash([]); } var items = []; if (this.show.bonds) d.bonds.forEach(function (b) { items.push({z: (S[b.a][2]+S[b.b][2])/2 - 0.01, bond: b}); }); d.atoms.forEach(function (a, i) { items.push({z: S[i][2], atom: i}); }); items.sort(function (u, v) { return u.z - v.z; }); var s = this.scale * this.zoom; items.forEach(function (it) { if (it.bond) { var a = S[it.bond.a], b = S[it.bond.b]; ctx.strokeStyle = 'rgba(200,200,200,' + (0.25 + 0.65 * (it.bond.confidence == null ? 1 : it.bond.confidence)) + ')'; ctx.lineWidth = Math.max(1.5, 0.12 * s); ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); } else { var i = it.atom, at = d.atoms[i], p = S[i], r = Math.max(3, 0.45 * at.radius * s); var g = ctx.createRadialGradient(p[0]-r/3, p[1]-r/3, r/6, p[0], p[1], r); g.addColorStop(0, '#ffffff'); g.addColorStop(0.25, at.color); g.addColorStop(1, '#000000'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p[0], p[1], r, 0, 2*Math.PI); ctx.fill(); if (self.show.labels) { ctx.fillStyle = '#e6edf3'; ctx.font = '11px monospace'; ctx.fillText(at.symbol + i, p[0]+r, p[1]-r); } if (self.show.forces && d.overlays && d.overlays.forces) { var f = d.overlays.forces[i], q = self.project([P[i][0]+f[0], P[i][1]+f[1], P[i][2]+f[2]]); ctx.strokeStyle = '#f0b429'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(p[0], p[1]); ctx.lineTo(q[0], q[1]); ctx.stroke(); } } }); ctx.fillStyle = '#9fb0c0'; ctx.font = '12px sans-serif'; ctx.fillText(d.formula + ' | ' + (d.evidence_state ? d.evidence_state.maturity + ' ' + d.evidence_state.label : ''), 10, 18); ctx.fillText('style does not imply validation', 10, this.c.height - 10); }; global.MatterViewer = MatterViewer; })(window);