tmepass000's picture
MATTER OS v0.1.0
87b48de verified
Raw History Blame Contribute Delete
6.07 kB
/* 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);