Public
Edited
May 21
1 fork
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
crystalView = {
// ─── Configuration ───
const [Nx, Ny, Nz] = [3, 3, 3];
const isoLevel = 0.3;
const a = 2;
const radius = 0.3;
const cameraFov = 45, cameraAspect = 1, cameraNear = 0.1, cameraFar = 1000;
const backgroundColor = 0xffffff;

// ─── Scene Setup ───
const scene = new three.Scene();
const camera = new three.PerspectiveCamera(cameraFov, cameraAspect, cameraNear, cameraFar);
camera.position.set(3, 3, 3);
const renderer = new three.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, width);
renderer.setPixelRatio(devicePixelRatio);
renderer.setClearColor(backgroundColor, 1);
renderer.localClippingEnabled = true;
const container = html`<div style="background:white; width:${width}px; height:${width}px;"></div>`;
container.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.enableZoom = true;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;
controls.enableDamping = true;

scene.add(new three.AmbientLight(0xffffff, 0.5));
const dl = new three.DirectionalLight(0xffffff, 0.8);
dl.position.set(5, 5, 5);
scene.add(dl);

// ─── Lattice Setup ───
const [lattice, basisName] = latticeKey.split(" | ");
const entry = lattices[lattice].bases[basisName];
const params = lattices[lattice].parameters.map(p => lattices[lattice].defaults[p]);
const vectors = (entry.vectors || lattices[lattice].vectors)(params);
const basis = entry.basis;
const species = entry.species;

const scaled = vectors.map(v => new three.Vector3(...v).multiplyScalar(a));
const scaledLengths = scaled.map(v => v.length());
const shift = scaled.reduce((acc, vec) => acc.addScaledVector(vec, -0.5), new three.Vector3());

// ─── Planes ───
const planes = [];
for (let i = 0; i < 3; i++) {
const normal = scaled[(i + 1) % 3].clone().cross(scaled[(i + 2) % 3]).normalize();
const half = scaled[i].clone().multiplyScalar(0.5);
planes.push(new three.Plane().setFromNormalAndCoplanarPoint(normal, half));
planes.push(new three.Plane().setFromNormalAndCoplanarPoint(normal.clone().negate(), half.clone().negate()));
}
planes.forEach(p => { if (p.distanceToPoint(new three.Vector3(0, 0, 0)) < 0) p.negate(); });
const exteriorPlanes = planes;
const interiorPlanes = planes.map(p => p.clone().negate());

// ─── Materials ───
const speciesMats = {};
species.forEach(sp => {
if (!speciesMats[sp]) {
const col = new three.Color(colorMap[sp]?.color ?? 0x008080);
speciesMats[sp] = {
outside: new three.MeshStandardMaterial({
color: col,
transparent: true,
opacity: 0.2,
side: three.DoubleSide,
clippingPlanes: interiorPlanes,
clipIntersection: true,
depthWrite: false
}),
inside: new three.MeshStandardMaterial({
color: col,
transparent: false,
opacity: 0.6,
side: three.DoubleSide,
clippingPlanes: exteriorPlanes,
clipIntersection: false,
depthWrite: true
})
};
}
});

// ─── Groups ───
const atomsGroup = new three.Group();
const cellEdgesGroup = new three.Group();
const supercellEdgesGroup = new three.Group();
scene.add(atomsGroup, cellEdgesGroup, supercellEdgesGroup);

// ─── Atoms ───
const atoms = [];
for (let ix = 0; ix <= Nx; ix++) {
for (let iy = 0; iy <= Ny; iy++) {
for (let iz = 0; iz <= Nz; iz++) {
basis.forEach(([u, v, w], i) => {
if ((ix < Nx || u === 0) && (iy < Ny || v === 0) && (iz < Nz || w === 0)) {
const fx = ix + u, fy = iy + v, fz = iz + w;
const pos = new three.Vector3()
.addScaledVector(scaled[0], fx)
.addScaledVector(scaled[1], fy)
.addScaledVector(scaled[2], fz)
.add(shift);
atoms.push(pos);
const sp = species[i];
const mats = speciesMats[sp];
const Z = colorMap[sp]?.Z || 20;
const r = radius * (0.5 + 0.5 * (1 - Math.exp(-Z / 20)));
const geo = new three.SphereGeometry(r, 32, 32);

const outMesh = new three.Mesh(geo, mats.outside);
outMesh.position.copy(pos);
outMesh.renderOrder = 0;
atomsGroup.add(outMesh);

const inMesh = new three.Mesh(geo, mats.inside);
inMesh.position.copy(pos);
inMesh.renderOrder = 1;
atomsGroup.add(inMesh);
}
});
}
}
}
// ─── Electron Density (only for Diamond) ───
if (showDensity) { // (lattice === "FCC" && basisName === "Diamond (C)") {
const Ngrid = 40;
// 1) Build density grid in fractional (u,v,w)
const grid = Array.from({ length: Ngrid + 1 }, (_, i) =>
Array.from({ length: Ngrid + 1 }, (_, j) =>
Array.from({ length: Ngrid + 1 }, (_, k) => {
const u = i / Ngrid, v = j / Ngrid, w = k / Ngrid;
const pos = new three.Vector3()
.addScaledVector(scaled[0], u)
.addScaledVector(scaled[1], v)
.addScaledVector(scaled[2], w)
.add(shift);
let rho = 0;
atoms.forEach(atomPos => {
const d2 = pos.distanceToSquared(atomPos);
rho += Math.exp(-5 * d2);
});
return rho;
})
)
);
const gradientGrid = Array.from({ length: Ngrid + 1 }, (_, i) =>
Array.from({ length: Ngrid + 1 }, (_, j) =>
Array.from({ length: Ngrid + 1 }, (_, k) => new three.Vector3())
)
);
for (let i = 1; i < Ngrid; i++) {
for (let j = 1; j < Ngrid; j++) {
for (let k = 1; k < Ngrid; k++) {
const dx = (grid[i+1][j][k] - grid[i-1][j][k]) * 0.5;
const dy = (grid[i][j+1][k] - grid[i][j-1][k]) * 0.5;
const dz = (grid[i][j][k+1] - grid[i][j][k-1]) * 0.5;
gradientGrid[i][j][k].set(dx, dy, dz).normalize();
}
}
}

// 2) Interpolate in fractional space (linear interpolation)
function interpolateVerts(p1, p2, i, j, k) {
const [dx1, dy1, dz1] = cubeVerts[p1];
const [dx2, dy2, dz2] = cubeVerts[p2];
const idx1 = i + dx1, idy1 = j + dy1, idz1 = k + dz1;
const idx2 = i + dx2, idy2 = j + dy2, idz2 = k + dz2;
const v1 = grid[idx1][idy1][idz1];
const v2 = grid[idx2][idy2][idz2];
const mu = (v2 === v1) ? 0 : (isoLevel - v1) / (v2 - v1);
// Interpolate position
const u = (idx1 + mu * (idx2 - idx1)) / Ngrid;
const v = (idy1 + mu * (idy2 - idy1)) / Ngrid;
const w = (idz1 + mu * (idz2 - idz1)) / Ngrid;
const pos = new three.Vector3()
.addScaledVector(scaled[0], u)
.addScaledVector(scaled[1], v)
.addScaledVector(scaled[2], w)
.add(shift);
// Interpolate normal (gradient vector)
const g1 = gradientGrid[idx1][idy1][idz1];
const g2 = gradientGrid[idx2][idy2][idz2];
const normal = g1.clone().lerp(g2, mu).normalize();
return { pos, normal };
}

// 3) Run Marching Cubes
const vertices = [];
const normals = [];
const faces = [];
for (let i = 0; i < Ngrid; i++) {
for (let j = 0; j < Ngrid; j++) {
for (let k = 0; k < Ngrid; k++) {
let cubeindex = 0;
for (let n = 0; n < 8; n++) {
const [dx, dy, dz] = cubeVerts[n];
const ii = (i + dx);
const jj = (j + dy);
const kk = (k + dz);
if (grid[ii][jj][kk] < isoLevel) cubeindex |= 1 << n;
}
const edges = edgeTable[cubeindex];
if (!edges) continue;
const vertList = [];
for (let e = 0; e < 12; e++) {
if (edges & (1 << e)) {
const [p1, p2] = edgeConnections[e];
vertList[e] = interpolateVerts(p1, p2, i, j, k);
}
}
for (let t = 0; triTable[cubeindex][t] !== -1; t += 3) {
const a = vertList[triTable[cubeindex][t]];
const b = vertList[triTable[cubeindex][t+1]];
const c = vertList[triTable[cubeindex][t+2]];
if (!a || !b || !c) continue;
const idx = vertices.length / 3;
// Push positions
vertices.push(
a.pos.x, a.pos.y, a.pos.z,
b.pos.x, b.pos.y, b.pos.z,
c.pos.x, c.pos.y, c.pos.z
);
// Push normals
normals.push(
a.normal.x, a.normal.y, a.normal.z,
b.normal.x, b.normal.y, b.normal.z,
c.normal.x, c.normal.y, c.normal.z
);
// Push face indices
faces.push(idx, idx+1, idx+2);
}

}
}
}
// 4) Build isosurface mesh
const isoGeometry = new three.BufferGeometry();
isoGeometry.setAttribute('position', new three.Float32BufferAttribute(vertices, 3));
isoGeometry.setAttribute('normal', new three.Float32BufferAttribute(normals, 3));
isoGeometry.setIndex(faces);
// merge nearby vertices to stitch faces together
if (three.BufferGeometryUtils) {
three.BufferGeometryUtils.mergeVertices(isoGeometry, 1e-6);
}
isoGeometry.computeVertexNormals();
const isoMaterial = new three.MeshStandardMaterial({
color: 0x00ffff,
side: three.DoubleSide,
transparent: true,
opacity: 0.5,
clippingPlanes: exteriorPlanes,
clipIntersection: false,
depthWrite: false
});
const isoMesh = new three.Mesh(isoGeometry, isoMaterial);
isoMesh.renderOrder = 1;
scene.add(isoMesh);
}


// ─── Helper to Create Corners ───
function createCorners(scaleVectors, signs) {
const corners = [];
signs.forEach(s1 => signs.forEach(s2 => signs.forEach(s3 => {
corners.push(
new three.Vector3()
.addScaledVector(scaleVectors[0], s1 / 2)
.addScaledVector(scaleVectors[1], s2 / 2)
.addScaledVector(scaleVectors[2], s3 / 2)
);
})));
return corners;
}

const corners = createCorners(scaled, [-1, 1]);
const superCorners = [
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], Nz-0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], Nz-0.5),
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], Nz-0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], Nz-0.5)
];

// ─── Draw Edges ───
const edgeMat = new three.LineBasicMaterial({ color: 0x222222, depthTest: true });
corners.forEach((c1, i) =>
corners.slice(i + 1).forEach(c2 => {
const d = c1.distanceTo(c2);
if (
Math.abs(d - scaledLengths[0]) < 1e-6 ||
Math.abs(d - scaledLengths[1]) < 1e-6 ||
Math.abs(d - scaledLengths[2]) < 1e-6
) {
const line = new three.Line(
new three.BufferGeometry().setFromPoints([c1, c2]),
edgeMat
);
line.renderOrder = 2;
cellEdgesGroup.add(line);
}
})
);

const superMat = new three.LineBasicMaterial({ color: 0xcccccc, depthTest: false });
superCorners.forEach((c1, i) =>
superCorners.slice(i + 1).forEach(c2 => {
const d = c1.distanceTo(c2);
if (
Math.abs(d - scaledLengths[0] * Nx) < 1e-6 ||
Math.abs(d - scaledLengths[1] * Ny) < 1e-6 ||
Math.abs(d - scaledLengths[2] * Nz) < 1e-6
) {
const superLine = new three.Line(
new three.BufferGeometry().setFromPoints([c1, c2]),
superMat
);
superLine.renderOrder = 1;
supercellEdgesGroup.add(superLine);
}
})
);
// ─── Animate ───
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();

return container;
}
Insert cell
lattices = ({
"FCC": {
parameters: ["a"],
defaults: { a: 1 },
vectors: ([a]) => [[a, 0, 0], [0, a, 0], [0, 0, a]],
bases: {
"Copper (Cu)": {
basis: [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]],
species: ["Cu", "Cu", "Cu", "Cu"],
description: "Standard FCC structure with a single copper atom at each face center"
},
"Diamond (C)": {
basis: [
[0, 0, 0], [0.25, 0.25, 0.25],
[0.5, 0.5, 0], [0.75, 0.75, 0.25],
[0.5, 0, 0.5], [0.75, 0.25, 0.75],
[0, 0.5, 0.5], [0.25, 0.75, 0.75]
],
species: Array(8).fill("C"),
description: "Diamond cubic structure formed by two interpenetrating FCC sublattices"
},
"Nickel (Ni)": {
basis: [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]],
species: ["Ni", "Ni", "Ni", "Ni"],
description: "Nickel adopts the FCC structure with one atom at each face center"
},
"NaCl (Rock Salt)": {
basis: [[0, 0, 0], [0.5, 0.5, 0.5]],
species: ["Na", "Cl"],
description: "Face-centered cubic structure with alternating Na and Cl atoms"
},
"Zincblende (ZnS)": {
basis: [
[0, 0, 0], [0.25, 0.25, 0.25],
[0.5, 0.5, 0], [0.75, 0.75, 0.25],
[0.5, 0, 0.5], [0.75, 0.25, 0.75],
[0, 0.5, 0.5], [0.25, 0.75, 0.75]
],
species: ["Zn", "S", "Zn", "S", "Zn", "S", "Zn", "S"],
description: "Zincblende (sphalerite) structure: same geometry as diamond but with alternating Zn and S atoms"
},
"Calcium Fluoride (CaF₂)": {
basis: [
[0, 0, 0], [0.25, 0.25, 0.25], [0.75, 0.25, 0.25],
[0.25, 0.75, 0.25], [0.25, 0.25, 0.75]
],
species: ["Ca", "F", "F", "F", "F"],
description: "Fluorite structure: Ca at corners, F atoms centered in tetrahedra"
}
}
},

"SC (Simple Cubic)": {
parameters: ["a"],
defaults: { a: 1 },
vectors: ([a]) => [[a, 0, 0], [0, a, 0], [0, 0, a]],
bases: {
"CsCl (B2)": {
basis: [[0, 0, 0], [0.5, 0.5, 0.5]],
species: ["Cs", "Cl"],
description: "Simple cubic structure with Cl at corners and Cs at the body center"
},
"Polonium (Po)": {
basis: [[0, 0, 0]],
species: ["Po"],
description: "One of the very few elements that crystallizes in the simple cubic lattice"
}
}
},
"BCC (Body-Centered Cubic)": {
parameters: ["a"],
defaults: { a: 1 },
vectors: ([a]) => [[a, 0, 0], [0, a, 0], [0, 0, a]],
bases: {
"Iron (Fe α-phase)": {
basis: [[0, 0, 0], [0.5, 0.5, 0.5]],
species: ["Fe", "Fe"],
description: "Body-centered cubic (BCC) structure typical of iron at room temperature"
}
}
},

"Perovskite": {
parameters: ["a"],
defaults: { a: 1 },
vectors: ([a]) => [[a, 0, 0], [0, a, 0], [0, 0, a]],
bases: {
"BaTiO₃": {
basis: [
[0.5, 0.5, 0.5], [0, 0, 0],
[0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]
],
species: ["Ba", "Ti", "O", "O", "O"],
description: "Classic perovskite structure with Ba at corners, Ti at center, and O on faces"
}
}
},

"Hexagonal": {
parameters: ["a", "c"],
defaults: { a: 1, c: 1.6 },
vectors: ([a, c]) => [
[a, 0, 0],
[-a/2, Math.sqrt(3)*a/2, 0],
[0, 0, c]
],
bases: {
"Graphene (C)": {
basis: [[0, 0, 0], [1/3, 2/3, 0]],
species: ["C", "C"],
description: "2D hexagonal lattice of carbon atoms with two atoms per unit cell"
},
"MoS₂ (2H phase)": {
basis: [[0.0, 0.0, 0.5], [1/3, 2/3, 0.58], [2/3, 1/3, 0.42]],
species: ["Mo", "S", "S"],
description: "Layered 2D structure with Mo sandwiched between two S layers"
},
"Zinc (Zn)": {
basis: [[0, 0, 0], [1/3, 2/3, 0.5]],
species: ["Zn", "Zn"],
description: "Zinc forms a hexagonal close-packed structure with two atoms per unit cell"
},
"Boron Nitride (h-BN)": {
basis: [[0, 0, 0], [1/3, 2/3, 0]],
species: ["B", "N"],
description: "Hexagonal boron nitride: structurally like graphene but alternating B and N atoms"
},
"Magnesium (Mg)": {
basis: [[0, 0, 0], [1/3, 2/3, 0.5]],
species: ["Mg", "Mg"],
description: "HCP structure typical of magnesium: ABAB stacking in hexagonal close-packing"
}
}
},

"Tetragonal": {
parameters: ["a", "c"],
defaults: { a: 1, c: 1.5 },
vectors: ([a, c]) => [[a, 0, 0], [0, a, 0], [0, 0, c]],
bases: {
"Rutile TiO₂": {
basis: [
[0, 0, 0], [0.5, 0.5, 0.5],
[0.3, 0.3, 0], [0.7, 0.7, 0],
[0.3, 0.7, 0.5], [0.7, 0.3, 0.5]
],
species: ["Ti", "Ti", "O", "O", "O", "O"],
description: "Each Ti is octahedrally coordinated by O atoms, forming a tetragonal structure"
},
"SnO₂ (Cassiterite)": {
basis: [
[0, 0, 0], [0.5, 0.5, 0.5],
[0.306, 0.306, 0], [0.694, 0.694, 0],
[0.306, 0.694, 0.5], [0.694, 0.306, 0.5]
],
species: ["Sn", "Sn", "O", "O", "O", "O"],
description: "Tetragonal rutile-type structure with tin and oxygen atoms"
},
"Strontium Titanate (SrTiO₃)": {
basis: [
[0, 0, 0], [0.5, 0.5, 0.5],
[0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]
],
species: ["Sr", "Ti", "O", "O", "O"],
description: "Perovskite-type structure with Sr at corners, Ti at center, and O atoms on faces"
}
}
},
Triclinic: {
parameters: ["a", "b", "c", "alpha", "beta", "gamma"],
defaults: { a: 1, b: 1.1, c: 1.2, alpha: 90, beta: 100, gamma: 95 },
vectors: ([a, b, c, alpha, beta, gamma]) => {
const rad = deg => deg * Math.PI / 180;
const α = rad(alpha), β = rad(beta), γ = rad(gamma);
const cosα = Math.cos(α), cosβ = Math.cos(β), cosγ = Math.cos(γ);
const sinγ = Math.sin(γ);
const v1 = [a, 0, 0];
const v2 = [b * cosγ, b * sinγ, 0];
const cx = c * cosβ;
const cy = c * (cosα - cosβ * cosγ) / sinγ;
const cz = Math.sqrt(c * c - cx * cx - cy * cy);
const v3 = [cx, cy, cz];
return [v1, v2, v3];
},
bases: {
"Kyanite (Al₂SiO₅)": {
basis: [
[0.0, 0.0, 0.0],
[0.25, 0.25, 0.25],
[0.5, 0.0, 0.5],
[0.75, 0.25, 0.75]
],
species: ["Al", "Si", "O", "O"],
description: "Triclinic example with Al, Si, and O atoms in a realistic mineral configuration"
}
}
},

"Rhombohedral": {
parameters: ["a", "alpha"],
defaults: { a: 1, alpha: 60 },
vectors: ([a, alpha]) => {
const rad = Math.PI / 180 * alpha;
const c = Math.cos(rad), s = Math.sin(rad);
return [
[a, 0, 0],
[a * c, a * s, 0],
[a * c, a * (c - c * c) / s, a * Math.sqrt(1 - 3 * c * c + 2 * c * c * c) / s]
];
},
bases: {
"Calcite (CaCO₃)": {
basis: [[0, 0, 0], [1/3, 2/3, 0.25], [2/3, 1/3, 0.75]],
species: ["Ca", "C", "O"],
description: "Rhombohedral structure with layered carbonate ions"
}
}
}
})
Insert cell
Insert cell
Insert cell
Insert cell
String(edgeTable)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more