Deploy MEIDNet Studio
Browse files- meidnet/studio/landing.html +33 -22
- site/assets/prism-hero.js +8 -65
- site/assets/prism-tour.js +454 -0
- site/assets/prism3d.js +188 -0
meidnet/studio/landing.html
CHANGED
|
@@ -72,13 +72,22 @@ h1.mh .ln{display:block}h1.mh .for{font-weight:700;letter-spacing:-.01em}
|
|
| 72 |
@media(max-width:560px){.method{grid-template-columns:repeat(2,minmax(0,1fr))}.method div:nth-child(odd){border-left:0}.method div:nth-child(4){border-left:1px solid var(--line)}.method div:nth-child(n+3){border-top:1px solid var(--line)}.method div:nth-child(odd)::before{display:none}.method div:nth-child(4)::before{display:block}}
|
| 73 |
.frac{display:inline-flex;flex-direction:column;align-items:center;vertical-align:middle;margin:0 .15em;line-height:1.15}
|
| 74 |
.frac>span:first-child{border-bottom:1px solid currentColor;padding:0 .2em .05em}.frac>span:last-child{padding:.08em .2em 0}
|
| 75 |
-
.
|
| 76 |
-
.
|
| 77 |
-
.
|
| 78 |
-
.
|
| 79 |
-
.
|
| 80 |
-
.
|
| 81 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
.layers{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:8px 0 28px}@media(max-width:900px){.layers{grid-template-columns:1fr 1fr}}@media(max-width:600px){.layers{grid-template-columns:1fr}}
|
| 83 |
.layers .card h2 small{display:block;font:700 11px/1.4 system-ui;letter-spacing:.12em;text-transform:uppercase;color:color-mix(in oklab,var(--accent,var(--p1)) 72%,var(--eyebrow-mix))}
|
| 84 |
h2.sec{font-size:24px;margin:0 0 6px}.sec-sub{color:var(--muted);margin:0 0 14px}
|
|
@@ -144,16 +153,21 @@ footer{color:var(--faint);font-size:13.5px;padding:10px 0 40px;display:flex;gap:
|
|
| 144 |
<div style="--k:var(--c-search)"><b>Validate</b><span class="eq">E<sub>hull</sub>(x<sup>*</sup>) ≤ ε</span></div>
|
| 145 |
</section>
|
| 146 |
|
| 147 |
-
<
|
| 148 |
-
<
|
| 149 |
-
|
| 150 |
-
</
|
| 151 |
-
<
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
<h2 class="sec">Contents</h2>
|
| 159 |
<p class="sec-sub">Background on multimodal learning, model architectures, the MEIDNet Studio, datasets, benchmarks and contributed results.</p>
|
|
@@ -211,11 +225,6 @@ footer{color:var(--faint);font-size:13.5px;padding:10px 0 40px;display:flex;gap:
|
|
| 211 |
// Inside the Hugging Face page this site runs in a frame, and GitHub, doi.org or arXiv refuse to load in one:
|
| 212 |
// open every link that leaves this site in a new tab.
|
| 213 |
document.querySelectorAll('a[href^="http"]').forEach(a=>{ try{ if(new URL(a.href).origin!==location.origin){ a.target='_blank'; a.rel='noopener'; } }catch(e){} });
|
| 214 |
-
// The tour video is WebM (VP9). A browser that cannot play it gets the poster, linked to the same tour drawn live in the Studio.
|
| 215 |
-
const v=document.getElementById('tour-video'), fb=document.getElementById('tour-fallback');
|
| 216 |
-
const fallback=()=>{ v.hidden=true; v.removeAttribute('src'); fb.hidden=false; };
|
| 217 |
-
if(!v.canPlayType('video/webm; codecs="vp9"') && !v.canPlayType('video/webm')) fallback();
|
| 218 |
-
else { v.querySelector('source').addEventListener('error', fallback); v.addEventListener('error', fallback); }
|
| 219 |
})();
|
| 220 |
(function(){ const d=document.documentElement; try{ const t=localStorage.getItem('meidnet_theme'); if(t) d.dataset.theme=t; }catch(e){}
|
| 221 |
const b=document.getElementById('theme'); const isDark=()=>d.dataset.theme==='dark'||(!d.dataset.theme&&matchMedia('(prefers-color-scheme: dark)').matches);
|
|
@@ -224,7 +233,9 @@ footer{color:var(--faint);font-size:13.5px;padding:10px 0 40px;display:flex;gap:
|
|
| 224 |
document.getElementById('copybib').onclick=async()=>{ const m=document.getElementById('copied'); try{ await navigator.clipboard.writeText(document.getElementById('bib').textContent); m.textContent='copied'; setTimeout(()=>m.textContent='',1500); }catch(e){ m.textContent='select the text below and copy it'; } };
|
| 225 |
})();
|
| 226 |
</script>
|
|
|
|
| 227 |
<script src="/docs/assets/prism-hero.js" defer></script>
|
|
|
|
| 228 |
<script src="/ask-prism.js" defer></script>
|
| 229 |
</body>
|
| 230 |
</html>
|
|
|
|
| 72 |
@media(max-width:560px){.method{grid-template-columns:repeat(2,minmax(0,1fr))}.method div:nth-child(odd){border-left:0}.method div:nth-child(4){border-left:1px solid var(--line)}.method div:nth-child(n+3){border-top:1px solid var(--line)}.method div:nth-child(odd)::before{display:none}.method div:nth-child(4)::before{display:block}}
|
| 73 |
.frac{display:inline-flex;flex-direction:column;align-items:center;vertical-align:middle;margin:0 .15em;line-height:1.15}
|
| 74 |
.frac>span:first-child{border-bottom:1px solid currentColor;padding:0 .2em .05em}.frac>span:last-child{padding:.08em .2em 0}
|
| 75 |
+
.tour3d{margin:4px 0 30px;border:1px solid var(--line);border-top:4px solid var(--t3c,var(--c-family));border-radius:16px;background:var(--card);padding:16px 16px 14px;transition:border-color .3s}
|
| 76 |
+
.tour3d h2{margin:0 0 2px}.tour3d .sec-sub{margin:0}
|
| 77 |
+
.t3-strip{list-style:none;margin:12px 0 10px;padding:0;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px}
|
| 78 |
+
.t3-strip button{position:relative;width:100%;border:2px solid var(--bc);background:var(--card);color:color-mix(in oklab,var(--bc) 78%,var(--ink));border-radius:999px;padding:8px 4px;font:700 14.5px/1 var(--display);cursor:pointer;overflow:hidden}
|
| 79 |
+
.t3-strip button.on{background:var(--bc);color:#fff}.t3-strip button:hover{box-shadow:0 4px 14px -8px var(--bc)}
|
| 80 |
+
.t3-strip button i{position:absolute;left:0;bottom:0;height:3px;width:0;background:rgba(255,255,255,.9)}
|
| 81 |
+
.t3-stage{position:relative;aspect-ratio:16/9;border-radius:12px;overflow:hidden;border:1px solid var(--line);background:radial-gradient(60% 70% at 72% 40%,rgba(99,102,241,.08),transparent 70%),radial-gradient(50% 60% at 20% 80%,rgba(20,184,166,.07),transparent 70%),var(--bg)}
|
| 82 |
+
.t3-stage canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
|
| 83 |
+
.t3-cap{font-size:16.5px;line-height:1.5;margin:12px 2px 12px;min-height:3em}
|
| 84 |
+
.t3-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
| 85 |
+
.t3-b{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:10px;padding:8px 12px;font:600 14px/1 system-ui,sans-serif;cursor:pointer}
|
| 86 |
+
.t3-b:hover{box-shadow:0 4px 14px -8px rgba(0,0,0,.5)}.t3-b.primary{background:var(--p1);border-color:var(--p1);color:#fff;min-width:100px}
|
| 87 |
+
.t3-prog{flex:1 1 160px;height:8px;border-radius:4px;background:var(--line);cursor:pointer;overflow:hidden}
|
| 88 |
+
.t3-prog i{display:block;height:100%;width:0;background:var(--t3c,var(--p1))}
|
| 89 |
+
.t3-time{font:500 13px/1 var(--mono);color:var(--muted)}.t3-open{font-weight:700;text-decoration:none;margin-left:auto}
|
| 90 |
+
@media(max-width:700px){.tour3d{padding:12px 10px}.t3-strip{grid-template-columns:repeat(4,minmax(0,1fr))}.t3-strip button{font-size:13px;padding:7px 2px}.t3-cap{font-size:15.5px}.t3-open{margin-left:0;width:100%}}
|
| 91 |
.layers{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:8px 0 28px}@media(max-width:900px){.layers{grid-template-columns:1fr 1fr}}@media(max-width:600px){.layers{grid-template-columns:1fr}}
|
| 92 |
.layers .card h2 small{display:block;font:700 11px/1.4 system-ui;letter-spacing:.12em;text-transform:uppercase;color:color-mix(in oklab,var(--accent,var(--p1)) 72%,var(--eyebrow-mix))}
|
| 93 |
h2.sec{font-size:24px;margin:0 0 6px}.sec-sub{color:var(--muted);margin:0 0 14px}
|
|
|
|
| 153 |
<div style="--k:var(--c-search)"><b>Validate</b><span class="eq">E<sub>hull</sub>(x<sup>*</sup>) ≤ ε</span></div>
|
| 154 |
</section>
|
| 155 |
|
| 156 |
+
<section class="tour3d" id="tour3d" aria-labelledby="t3-title">
|
| 157 |
+
<h2 class="sec" id="t3-title">MEIDNet in one minute</h2>
|
| 158 |
+
<p class="sec-sub">From the properties you want to candidate crystals, block by block. Click a block to jump to it.</p>
|
| 159 |
+
<ol class="t3-strip" aria-label="Chapters"></ol>
|
| 160 |
+
<div class="t3-stage"><canvas tabindex="0" role="img" aria-label="Animated tour of MEIDNet in three dimensions: data, model, family, rules, targets, search and candidates. The caption below gives each step in words."></canvas></div>
|
| 161 |
+
<p class="t3-cap" aria-live="polite"></p>
|
| 162 |
+
<div class="t3-bar">
|
| 163 |
+
<button type="button" class="t3-b primary" data-t3="play" aria-label="Play the tour">▶ Play</button>
|
| 164 |
+
<button type="button" class="t3-b" data-t3="prev" aria-label="Previous chapter">◀</button>
|
| 165 |
+
<button type="button" class="t3-b" data-t3="next" aria-label="Next chapter">▶</button>
|
| 166 |
+
<div class="t3-prog" role="slider" tabindex="0" aria-label="Position in the tour, in seconds" aria-valuemin="0" aria-valuemax="63" aria-valuenow="0"><i></i></div>
|
| 167 |
+
<span class="t3-time">0:00</span>
|
| 168 |
+
<a class="t3-open" href="/studio/">Open the Studio →</a>
|
| 169 |
+
</div>
|
| 170 |
+
</section>
|
| 171 |
|
| 172 |
<h2 class="sec">Contents</h2>
|
| 173 |
<p class="sec-sub">Background on multimodal learning, model architectures, the MEIDNet Studio, datasets, benchmarks and contributed results.</p>
|
|
|
|
| 225 |
// Inside the Hugging Face page this site runs in a frame, and GitHub, doi.org or arXiv refuse to load in one:
|
| 226 |
// open every link that leaves this site in a new tab.
|
| 227 |
document.querySelectorAll('a[href^="http"]').forEach(a=>{ try{ if(new URL(a.href).origin!==location.origin){ a.target='_blank'; a.rel='noopener'; } }catch(e){} });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
})();
|
| 229 |
(function(){ const d=document.documentElement; try{ const t=localStorage.getItem('meidnet_theme'); if(t) d.dataset.theme=t; }catch(e){}
|
| 230 |
const b=document.getElementById('theme'); const isDark=()=>d.dataset.theme==='dark'||(!d.dataset.theme&&matchMedia('(prefers-color-scheme: dark)').matches);
|
|
|
|
| 233 |
document.getElementById('copybib').onclick=async()=>{ const m=document.getElementById('copied'); try{ await navigator.clipboard.writeText(document.getElementById('bib').textContent); m.textContent='copied'; setTimeout(()=>m.textContent='',1500); }catch(e){ m.textContent='select the text below and copy it'; } };
|
| 234 |
})();
|
| 235 |
</script>
|
| 236 |
+
<script src="/docs/assets/prism3d.js" defer></script>
|
| 237 |
<script src="/docs/assets/prism-hero.js" defer></script>
|
| 238 |
+
<script src="/docs/assets/prism-tour.js" defer></script>
|
| 239 |
<script src="/ask-prism.js" defer></script>
|
| 240 |
</body>
|
| 241 |
</html>
|
site/assets/prism-hero.js
CHANGED
|
@@ -5,11 +5,7 @@
|
|
| 5 |
"use strict";
|
| 6 |
var NS = "http://www.w3.org/2000/svg";
|
| 7 |
|
| 8 |
-
|
| 9 |
-
var t = document.documentElement.getAttribute("data-theme");
|
| 10 |
-
if (t) return t === "dark";
|
| 11 |
-
return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
| 12 |
-
}
|
| 13 |
|
| 14 |
/* ═══════════ the lettering: one glyph skeleton, two renderings ═══════════
|
| 15 |
"neural": nodes at regular spacing along the strokes, thin connections, faint synapses inside each letter
|
|
@@ -182,21 +178,11 @@
|
|
| 182 |
var ctx = canvas.getContext("2d");
|
| 183 |
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 184 |
|
| 185 |
-
//
|
| 186 |
-
var
|
| 187 |
-
var X = [[a, 0, 0], [-a, 0, 0], [0, a, 0], [0, -a, 0], [0, 0, a], [0, 0, -a]];
|
| 188 |
-
[-a, a].forEach(function (x) { [-a, a].forEach(function (y) { [-a, a].forEach(function (z) { A.push([x, y, z]); }); }); });
|
| 189 |
-
A.forEach(function (p) { A.forEach(function (q) { // the cube: corners one lattice step apart
|
| 190 |
-
var d = Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
|
| 191 |
-
if (Math.abs(d - 2 * a) < 1e-6 && p < q) EDGES.push([p, q]);
|
| 192 |
-
}); });
|
| 193 |
-
[[0, 2, 4], [0, 4, 3], [0, 3, 5], [0, 5, 2], [1, 4, 2], [1, 3, 4], [1, 5, 3], [1, 2, 5]].forEach(function (f) {
|
| 194 |
-
FACES.push([X[f[0]], X[f[1]], X[f[2]]]);
|
| 195 |
-
});
|
| 196 |
|
| 197 |
// panels behind the crystal: centre, half sizes, rotation about y, what they show
|
| 198 |
-
|
| 199 |
-
var r = rng(7), LATENT = [];
|
| 200 |
for (var i = 0; i < 90; i++) { // paired points: a structure (teal) and its properties (violet), close together
|
| 201 |
var t = r() * 2 - 1, u = r() * 2 - 1, cx = 0.75 * t, cy = 0.55 * Math.sin(2.2 * t) * 0.8 + 0.25 * u;
|
| 202 |
LATENT.push({u: cx, v: cy, du: (r() - 0.5) * 0.09, dv: (r() - 0.5) * 0.09, c: t});
|
|
@@ -209,7 +195,7 @@
|
|
| 209 |
{c: [2.25, -1.25, -1.6], h: [0.8, 0.56], ry: -0.62, kind: "property", label: "property: band gap"}
|
| 210 |
];
|
| 211 |
|
| 212 |
-
var W = 0, H = 0, DPR = 1, yaw = 0, pitch = 0, dragYaw = 0, dragPitch = 0, t0 = performance.now();
|
| 213 |
function resize() {
|
| 214 |
var b = canvas.getBoundingClientRect();
|
| 215 |
DPR = Math.min(2, window.devicePixelRatio || 1);
|
|
@@ -217,28 +203,13 @@
|
|
| 217 |
canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
|
| 218 |
draw();
|
| 219 |
}
|
| 220 |
-
function proj(p) {
|
| 221 |
-
var cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch);
|
| 222 |
-
var x1 = p[0] * cy + p[2] * sy, z1 = -p[0] * sy + p[2] * cy, y1 = p[1];
|
| 223 |
-
var y2 = y1 * cp - z1 * sp, z2 = y1 * sp + z1 * cp;
|
| 224 |
-
var D = 9, F = Math.min(W, H * 1.1) * D / 8.3, k = F / (D - z2);
|
| 225 |
-
return [W * 0.52 + x1 * k, H * 0.5 - y2 * k, z2, k];
|
| 226 |
-
}
|
| 227 |
function panelPoint(P, u, v, w) { // panel-local (u, v in [-1, 1]) to world
|
| 228 |
var c = Math.cos(P.ry), sn = Math.sin(P.ry), x = u * P.h[0], y = v * P.h[1];
|
| 229 |
return [P.c[0] + x * c + (w || 0) * sn, P.c[1] + y, P.c[2] - x * sn + (w || 0) * c];
|
| 230 |
}
|
| 231 |
function poly(pts) { ctx.beginPath(); pts.forEach(function (p, j) { j ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]); }); ctx.closePath(); }
|
| 232 |
|
| 233 |
-
function sphere(p, rad, col, dark) {
|
| 234 |
-
var q = proj(p), R = rad * q[3];
|
| 235 |
-
var g = ctx.createRadialGradient(q[0] - R * 0.38, q[1] - R * 0.42, R * 0.08, q[0], q[1], R);
|
| 236 |
-
g.addColorStop(0, "#ffffff"); g.addColorStop(0.22, col[0]); g.addColorStop(0.72, col[1]); g.addColorStop(1, col[2]);
|
| 237 |
-
ctx.beginPath(); ctx.arc(q[0], q[1], R, 0, 2 * Math.PI);
|
| 238 |
-
ctx.fillStyle = g; ctx.fill();
|
| 239 |
-
ctx.lineWidth = 0.8; ctx.strokeStyle = dark ? "rgba(0,0,0,.35)" : "rgba(15,23,42,.12)"; ctx.stroke();
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
function drawPanel(P, dark) {
|
| 243 |
var corners = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(function (c) { return proj(panelPoint(P, c[0], c[1])); });
|
| 244 |
poly(corners);
|
|
@@ -291,6 +262,7 @@
|
|
| 291 |
function draw() {
|
| 292 |
if (!W) return;
|
| 293 |
var dark = isDark();
|
|
|
|
| 294 |
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
| 295 |
ctx.clearRect(0, 0, W, H);
|
| 296 |
// floor grid
|
|
@@ -305,36 +277,7 @@
|
|
| 305 |
ctx.fillStyle = sh; ctx.beginPath(); ctx.ellipse(f[0], f[1], 2.0 * f[3], 0.62 * f[3], 0, 0, 2 * Math.PI); ctx.fill();
|
| 306 |
// panels, far first
|
| 307 |
PANELS.slice().sort(function (p, q) { return proj(p.c)[2] - proj(q.c)[2]; }).forEach(function (P) { drawPanel(P, dark); });
|
| 308 |
-
|
| 309 |
-
var items = [];
|
| 310 |
-
FACES.forEach(function (fc) {
|
| 311 |
-
var q = fc.map(proj), z = (q[0][2] + q[1][2] + q[2][2]) / 3;
|
| 312 |
-
items.push({z: z, f: fc, q: q});
|
| 313 |
-
});
|
| 314 |
-
B.forEach(function (p) { items.push({z: proj(p)[2], p: p, rad: 0.27, col: ["#93c5fd", "#3b82f6", "#1e3a8a"]}); });
|
| 315 |
-
X.forEach(function (p) { items.push({z: proj(p)[2], p: p, rad: 0.2, col: ["#99f6e4", "#14b8a6", "#115e59"]}); });
|
| 316 |
-
A.forEach(function (p) { items.push({z: proj(p)[2], p: p, rad: 0.25, col: ["#c4b5fd", "#8b5cf6", "#3b0764"]}); });
|
| 317 |
-
EDGES.forEach(function (e) { var q = [proj(e[0]), proj(e[1])]; items.push({z: (q[0][2] + q[1][2]) / 2 - 0.4, line: q}); });
|
| 318 |
-
items.sort(function (m, n) { return m.z - n.z; });
|
| 319 |
-
var Ldir = [-0.45, 0.7, 0.55];
|
| 320 |
-
items.forEach(function (it) {
|
| 321 |
-
if (it.p) return sphere(it.p, it.rad, it.col, dark);
|
| 322 |
-
if (it.line) {
|
| 323 |
-
ctx.lineWidth = 1.2; ctx.strokeStyle = dark ? "rgba(148,163,184,.45)" : "rgba(100,116,139,.45)";
|
| 324 |
-
ctx.beginPath(); ctx.moveTo(it.line[0][0], it.line[0][1]); ctx.lineTo(it.line[1][0], it.line[1][1]); ctx.stroke(); return;
|
| 325 |
-
}
|
| 326 |
-
var v = it.f, e1 = [v[1][0] - v[0][0], v[1][1] - v[0][1], v[1][2] - v[0][2]], e2 = [v[2][0] - v[0][0], v[2][1] - v[0][1], v[2][2] - v[0][2]];
|
| 327 |
-
var n = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]], nl = Math.hypot(n[0], n[1], n[2]);
|
| 328 |
-
var cy = Math.cos(yaw), sy = Math.sin(yaw), nx = (n[0] * cy + n[2] * sy) / nl, nz = (-n[0] * sy + n[2] * cy) / nl, ny = n[1] / nl;
|
| 329 |
-
var lit = Math.abs(nx * Ldir[0] + ny * Ldir[1] + nz * Ldir[2]);
|
| 330 |
-
poly(it.q);
|
| 331 |
-
var gr = ctx.createLinearGradient(it.q[0][0], it.q[0][1], it.q[2][0], it.q[2][1]);
|
| 332 |
-
gr.addColorStop(0, dark ? "rgba(147,197,253," + (0.1 + 0.22 * lit).toFixed(3) + ")" : "rgba(255,255,255," + (0.25 + 0.45 * lit).toFixed(3) + ")");
|
| 333 |
-
gr.addColorStop(1, dark ? "rgba(59,130,246," + (0.08 + 0.14 * lit).toFixed(3) + ")" : "rgba(129,161,250," + (0.12 + 0.22 * lit).toFixed(3) + ")");
|
| 334 |
-
ctx.fillStyle = gr; ctx.fill();
|
| 335 |
-
ctx.lineWidth = 1; ctx.strokeStyle = dark ? "rgba(191,219,254,.5)" : "rgba(255,255,255,.95)"; ctx.stroke();
|
| 336 |
-
ctx.lineWidth = 0.6; ctx.strokeStyle = dark ? "rgba(96,165,250,.35)" : "rgba(99,102,241,.25)"; ctx.stroke();
|
| 337 |
-
});
|
| 338 |
}
|
| 339 |
|
| 340 |
var running = false, visible = true;
|
|
|
|
| 5 |
"use strict";
|
| 6 |
var NS = "http://www.w3.org/2000/svg";
|
| 7 |
|
| 8 |
+
var P3 = window.Prism3D, isDark = P3.isDark;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
/* ═══════════ the lettering: one glyph skeleton, two renderings ═══════════
|
| 11 |
"neural": nodes at regular spacing along the strokes, thin connections, faint synapses inside each letter
|
|
|
|
| 178 |
var ctx = canvas.getContext("2d");
|
| 179 |
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 180 |
|
| 181 |
+
// the cubic ABX3 cell (A on the corners, the BX6 octahedron in the centre), half edge 1.25 in this scene
|
| 182 |
+
var CELL = P3.perovskite({A: "A", B: "B", X: "X"});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
// panels behind the crystal: centre, half sizes, rotation about y, what they show
|
| 185 |
+
var r = P3.T.rng(7), LATENT = [];
|
|
|
|
| 186 |
for (var i = 0; i < 90; i++) { // paired points: a structure (teal) and its properties (violet), close together
|
| 187 |
var t = r() * 2 - 1, u = r() * 2 - 1, cx = 0.75 * t, cy = 0.55 * Math.sin(2.2 * t) * 0.8 + 0.25 * u;
|
| 188 |
LATENT.push({u: cx, v: cy, du: (r() - 0.5) * 0.09, dv: (r() - 0.5) * 0.09, c: t});
|
|
|
|
| 195 |
{c: [2.25, -1.25, -1.6], h: [0.8, 0.56], ry: -0.62, kind: "property", label: "property: band gap"}
|
| 196 |
];
|
| 197 |
|
| 198 |
+
var W = 0, H = 0, DPR = 1, yaw = 0, pitch = 0, dragYaw = 0, dragPitch = 0, t0 = performance.now(), cam = null;
|
| 199 |
function resize() {
|
| 200 |
var b = canvas.getBoundingClientRect();
|
| 201 |
DPR = Math.min(2, window.devicePixelRatio || 1);
|
|
|
|
| 203 |
canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
|
| 204 |
draw();
|
| 205 |
}
|
| 206 |
+
function proj(p) { return cam.proj(p); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
function panelPoint(P, u, v, w) { // panel-local (u, v in [-1, 1]) to world
|
| 208 |
var c = Math.cos(P.ry), sn = Math.sin(P.ry), x = u * P.h[0], y = v * P.h[1];
|
| 209 |
return [P.c[0] + x * c + (w || 0) * sn, P.c[1] + y, P.c[2] - x * sn + (w || 0) * c];
|
| 210 |
}
|
| 211 |
function poly(pts) { ctx.beginPath(); pts.forEach(function (p, j) { j ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]); }); ctx.closePath(); }
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
function drawPanel(P, dark) {
|
| 214 |
var corners = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(function (c) { return proj(panelPoint(P, c[0], c[1])); });
|
| 215 |
poly(corners);
|
|
|
|
| 262 |
function draw() {
|
| 263 |
if (!W) return;
|
| 264 |
var dark = isDark();
|
| 265 |
+
cam = P3.camera({cx: W * 0.52, cy: H * 0.5, scale: Math.min(W, H * 1.1) / 8.3, yaw: yaw, pitch: pitch});
|
| 266 |
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
| 267 |
ctx.clearRect(0, 0, W, H);
|
| 268 |
// floor grid
|
|
|
|
| 277 |
ctx.fillStyle = sh; ctx.beginPath(); ctx.ellipse(f[0], f[1], 2.0 * f[3], 0.62 * f[3], 0, 0, 2 * Math.PI); ctx.fill();
|
| 278 |
// panels, far first
|
| 279 |
PANELS.slice().sort(function (p, q) { return proj(p.c)[2] - proj(q.c)[2]; }).forEach(function (P) { drawPanel(P, dark); });
|
| 280 |
+
P3.crystal(ctx, cam, CELL, {s: 1.25, atomScale: 0.6, dark: dark});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
var running = false, visible = true;
|
site/assets/prism-tour.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* The MEIDNet tour on the landing page: the one-minute story of the Studio (data → model → family → rules → targets →
|
| 2 |
+
search → candidates) as a live 3D animation. Every frame is a pure function of time, so playing, seeking, the
|
| 3 |
+
chapter buttons and reduced motion all show the same pictures. The numbers are those of the Studio with its
|
| 4 |
+
defaults: the published Perov-5 model, the cubic halide perovskite family, targets 2 eV and −0.1 eV/atom. */
|
| 5 |
+
(function () {
|
| 6 |
+
"use strict";
|
| 7 |
+
var P = window.Prism3D, root = document.getElementById("tour3d");
|
| 8 |
+
if (!P || !root) return;
|
| 9 |
+
var T = P.T, canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d");
|
| 10 |
+
var VW = 960, VH = 540;
|
| 11 |
+
|
| 12 |
+
var BLOCKS = [{id: "data", name: "Data", c: "#2a78d6"}, {id: "model", name: "Model", c: "#eb6834"}, {id: "family", name: "Family", c: "#1baf7a"},
|
| 13 |
+
{id: "rules", name: "Rules", c: "#eda100"}, {id: "targets", name: "Targets", c: "#e87ba4"}, {id: "search", name: "Search", c: "#008300"},
|
| 14 |
+
{id: "candidates", name: "Candidates", c: "#4a3aa7"}];
|
| 15 |
+
var COL = {}; BLOCKS.forEach(function (b) { COL[b.id] = b.c; });
|
| 16 |
+
var th = P.theme();
|
| 17 |
+
function tc(id) { // a block colour readable as text on this theme
|
| 18 |
+
var c = P.rgbOf(COL[id] || COL.family);
|
| 19 |
+
return P.css(th.dark ? P.mix(c, [255, 255, 255], 0.3) : P.mix(c, [0, 0, 0], id === "rules" ? 0.3 : id === "targets" ? 0.2 : 0.05));
|
| 20 |
+
}
|
| 21 |
+
var sub = function (f) { return String(f).replace(/\d/g, function (d) { return "₀₁₂₃₄₅₆₇₈₉"[+d]; }); };
|
| 22 |
+
var fmt = function (v) { return (Math.round(v * 100) / 100).toString().replace("-", "−"); };
|
| 23 |
+
|
| 24 |
+
/* ── the facts (read from the Studio with its defaults) ── */
|
| 25 |
+
var ROWS = [{f: "SrTiO3", e: {A: "Sr", B: "Ti", X: "O"}, v: [-0.24, 4.3]}, {f: "KNbO3", e: {A: "K", B: "Nb", X: "O"}, v: [-0.42, 4.1]},
|
| 26 |
+
{f: "LaTiNO2", e: {A: "La", B: "Ti", X: ["N", "O", "O"]}, v: [0.06, 2.5]}, {f: "SrZrNOF", e: {A: "Sr", B: "Zr", X: ["N", "O", "F"]}, v: [0.54, 3.2]}];
|
| 27 |
+
var GROUPS = [{g: "A", sample: ["Ba", "Sr", "Ca", "Na", "K", "Rb", "Cs", "La", "Ce", "Pr", "Nd", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu"]},
|
| 28 |
+
{g: "B", sample: ["Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Sn", "Pb", "Sc", "Cr", "V"]}, {g: "X", sample: ["F", "Cl", "Br", "I"]}];
|
| 29 |
+
var RULES = [{name: "charges balance", lost: 796}, {name: "tolerance factor", lost: 60}, {name: "octahedral factor", lost: 41}];
|
| 30 |
+
// compositions on the conveyor: the rule that stops each one (gate index), or -1 when it passes every rule
|
| 31 |
+
var FLOW = [{f: "BaMnF3", e: {A: "Ba", B: "Mn", X: "F"}, gate: 0}, {f: "NaFeI3", e: {A: "Na", B: "Fe", X: "I"}, gate: -1},
|
| 32 |
+
{f: "NaPbF3", e: {A: "Na", B: "Pb", X: "F"}, gate: 1}, {f: "NaMnF3", e: {A: "Na", B: "Mn", X: "F"}, gate: 2},
|
| 33 |
+
{f: "KFeI3", e: {A: "K", B: "Fe", X: "I"}, gate: -1}, {f: "BaMnCl3", e: {A: "Ba", B: "Mn", X: "Cl"}, gate: 0},
|
| 34 |
+
{f: "KCoI3", e: {A: "K", B: "Co", X: "I"}, gate: -1}, {f: "BaFeF3", e: {A: "Ba", B: "Fe", X: "F"}, gate: 0}];
|
| 35 |
+
// the 27 compositions that pass every rule: predicted formation enthalpy (eV/atom) and direct band gap (eV)
|
| 36 |
+
var ALIVE = [["CsMnI3", 1.9, 1.57], ["CsPbI3", 2.05, 3.19], ["CsZnI3", 2.07, 2.58], ["KCoI3", 1.29, 1.82], ["KCuI3", 1.85, 1.44],
|
| 37 |
+
["KFeI3", 0.96, 1.74], ["KMnI3", 1.71, 0.59], ["KNiI3", 1.86, 1.04], ["KPbI3", 2.01, 2.02], ["KSnI3", 1.72, 1.87],
|
| 38 |
+
["KZnI3", 2.09, 1.87], ["NaCoBr3", 0.96, 2.82], ["NaCoI3", 0.95, 2.69], ["NaCuI3", 1.45, 3.49], ["NaFeI3", 0.95, 2.08],
|
| 39 |
+
["NaMnI3", 1.84, 1.66], ["NaNiBr3", 1.31, 3.04], ["NaNiI3", 1.18, 2.92], ["NaSnI3", 1.71, 2.98], ["NaZnI3", 1.98, 1.51],
|
| 40 |
+
["RbCoI3", 1.71, 1.18], ["RbCuI3", 2.09, 2.07], ["RbFeI3", 1.46, 1.49], ["RbMnI3", 1.74, 2.41], ["RbPbI3", 2.01, 2.32],
|
| 41 |
+
["RbSnI3", 1.91, 1.83], ["RbZnI3", 2.02, 2.81]];
|
| 42 |
+
var RANKED = ["NaFeI3", "KFeI3", "KCoI3"];
|
| 43 |
+
// a search with the Studio defaults (seed 937): the three crystals it returned, with their predicted properties
|
| 44 |
+
var FOUND = [{f: "NaNiI3", e: {A: "Na", B: "Ni", X: "I"}, gap: 1.97, dh: -0.05, flag: false},
|
| 45 |
+
{f: "NaSnI3", e: {A: "Na", B: "Sn", X: "I"}, gap: 1.98, dh: -1.37, flag: true},
|
| 46 |
+
{f: "NaMnI3", e: {A: "Na", B: "Mn", X: "I"}, gap: 2.37, dh: 14.3, flag: true}];
|
| 47 |
+
var STOPPED = [{e: {A: "Ba", B: "Mn", X: "F"}}, {e: {A: "Na", B: "Pb", X: "F"}}, {e: {A: "Na", B: "Mn", X: "F"}}, {e: {A: "Ba", B: "Mn", X: "Cl"}}, {e: {A: "Ba", B: "Fe", X: "F"}}];
|
| 48 |
+
var cells = {};
|
| 49 |
+
function cell(e) { var k = JSON.stringify(e); return cells[k] || (cells[k] = P.perovskite(e)); }
|
| 50 |
+
|
| 51 |
+
/* ── flat helpers in the 960 × 540 stage ── */
|
| 52 |
+
function card(x, y, w, h, o) {
|
| 53 |
+
o = o || {};
|
| 54 |
+
ctx.save(); if (o.alpha !== undefined) ctx.globalAlpha *= T.clamp(o.alpha);
|
| 55 |
+
P.rrect(ctx, x, y, w, h, o.r === undefined ? 14 : o.r);
|
| 56 |
+
ctx.fillStyle = o.fill || (th.dark ? "rgba(27,31,36,.86)" : "rgba(255,255,255,.88)"); ctx.fill();
|
| 57 |
+
if (o.stroke !== null) { ctx.lineWidth = o.sw || 1.5; ctx.strokeStyle = o.stroke || th.line; ctx.stroke(); }
|
| 58 |
+
ctx.restore();
|
| 59 |
+
}
|
| 60 |
+
function tint(id, a) { return P.css(P.rgbOf(COL[id]), a); }
|
| 61 |
+
function label(str, x, y, o) { o = o || {}; P.text(ctx, str, x, y, {size: o.size || 18, weight: o.weight || 600, color: o.color || th.ink, align: o.align, alpha: o.alpha, family: o.family, italic: o.italic, baseline: o.baseline}); }
|
| 62 |
+
function cam(cx, cy, scale, yaw, pitch) { return P.camera({cx: cx, cy: cy, scale: scale, yaw: yaw, pitch: pitch === undefined ? -0.38 : pitch}); }
|
| 63 |
+
function chip(x, y, str, id, on, o) {
|
| 64 |
+
o = o || {}; var w = o.w || Math.max(46, 12 * str.length + 26), h = o.h || 32;
|
| 65 |
+
card(x - w / 2, y - h / 2, w, h, {r: h / 2, fill: on ? COL[id] : (th.dark ? "#1b1f24" : "#fff"), stroke: COL[id], sw: 2, alpha: o.alpha});
|
| 66 |
+
label(str, x, y + 6, {align: "center", size: o.size || 17, weight: 700, color: on ? "#fff" : tc(id), alpha: o.alpha});
|
| 67 |
+
}
|
| 68 |
+
function star(x, y, r, color, a) {
|
| 69 |
+
ctx.save(); ctx.globalAlpha *= a === undefined ? 1 : a; ctx.beginPath();
|
| 70 |
+
for (var i = 0; i < 10; i++) { var rr = i % 2 ? r * 0.45 : r, an = -Math.PI / 2 + i * Math.PI / 5; ctx.lineTo(x + rr * Math.cos(an), y + rr * Math.sin(an)); }
|
| 71 |
+
ctx.closePath(); ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = 12; ctx.fill(); ctx.restore();
|
| 72 |
+
}
|
| 73 |
+
function dot(cm, p, r, color, a) {
|
| 74 |
+
var q = cm.proj(p); ctx.save(); ctx.globalAlpha *= a === undefined ? 1 : a;
|
| 75 |
+
ctx.beginPath(); ctx.arc(q[0], q[1], Math.max(1, r * q[3]), 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); ctx.restore(); return q;
|
| 76 |
+
}
|
| 77 |
+
function cloud(seed, n, radius) {
|
| 78 |
+
var R = T.rng(seed), pts = [];
|
| 79 |
+
while (pts.length < n) { var p = [R() * 2 - 1, R() * 2 - 1, R() * 2 - 1]; if (p[0] * p[0] + p[1] * p[1] + p[2] * p[2] <= 1) pts.push(p.map(function (v) { return v * radius; })); }
|
| 80 |
+
return pts;
|
| 81 |
+
}
|
| 82 |
+
var DATA_CLOUD = cloud(21, 320, 1.25), MAP_CLOUD = cloud(5, 170, 1.7);
|
| 83 |
+
|
| 84 |
+
/* ── scenes ── */
|
| 85 |
+
var SCENES = [
|
| 86 |
+
{block: "intro", title: "Start", ms: 4800,
|
| 87 |
+
cap: "Tell MEIDNet the properties you want and it suggests crystals that should have them. Here is how, block by block.",
|
| 88 |
+
draw: function (t) {
|
| 89 |
+
var a = T.out(T.seg(t, 0, 0.22));
|
| 90 |
+
ctx.save(); ctx.translate(-50 * (1 - a), 0);
|
| 91 |
+
card(64, 168, 316, 196, {fill: th.dark ? "rgba(232,123,164,.12)" : "rgba(253,237,243,.92)", stroke: COL.targets, sw: 2.5, r: 20, alpha: a});
|
| 92 |
+
label("You want", 92, 216, {size: 24, color: th.muted, alpha: a});
|
| 93 |
+
label("Direct band gap", 92, 266, {size: 31, weight: 800, color: tc("targets"), alpha: a});
|
| 94 |
+
label("≈ 2 eV", 92, 324, {size: 42, weight: 800, alpha: a});
|
| 95 |
+
ctx.restore();
|
| 96 |
+
label("MEIDNet", 500, 238, {size: 30, weight: 800, color: tc("family"), align: "center", alpha: T.seg(t, 0.22, 0.4)});
|
| 97 |
+
P.arrow(ctx, 402, 266, 600, 266, T.ease(T.seg(t, 0.22, 0.46)), COL.family, 4.5);
|
| 98 |
+
var c = cam(770, 268, 80, -0.7 + 1.2 * t);
|
| 99 |
+
P.crystal(ctx, c, cell(FOUND[0].e), {s: 1, explode: 1 - T.ease(T.seg(t, 0.32, 0.72)), glass: T.seg(t, 0.62, 0.82), edges: T.seg(t, 0.62, 0.82),
|
| 100 |
+
labels: t > 0.6, dark: th.dark, alpha: T.seg(t, 0.28, 0.4)});
|
| 101 |
+
label("NaNiI₃", 770, 420, {size: 20, weight: 700, color: th.muted, align: "center", alpha: T.seg(t, 0.75, 0.9)});
|
| 102 |
+
label("inverse design: from properties to crystals", 480, 492, {size: 25, color: th.muted, align: "center", alpha: T.seg(t, 0.6, 0.82)});
|
| 103 |
+
}},
|
| 104 |
+
|
| 105 |
+
{block: "data", title: "Data", ms: 7000,
|
| 106 |
+
cap: "MEIDNet learns from known materials: a table in which each row is a crystal structure with its properties. Here, 11,356 materials.",
|
| 107 |
+
draw: function (t) {
|
| 108 |
+
card(32, 60, 600, 420);
|
| 109 |
+
var hd = {size: 15, weight: 700, color: th.muted};
|
| 110 |
+
label("structure", 56, 98, hd); label("formula", 168, 98, hd); label("formation enthalpy", 330, 90, hd); label("eV/atom", 330, 110, {size: 13, color: th.faint});
|
| 111 |
+
label("band gap", 510, 90, hd); label("eV", 510, 110, {size: 13, color: th.faint});
|
| 112 |
+
ctx.fillStyle = th.line; ctx.fillRect(48, 124, 568, 1.5);
|
| 113 |
+
ROWS.forEach(function (r, k) {
|
| 114 |
+
var p = T.out(T.seg(t, 0.05 + k * 0.1, 0.27 + k * 0.1)), y = 130 + k * 80;
|
| 115 |
+
ctx.save(); ctx.translate(70 * (1 - p), 0); ctx.globalAlpha *= p;
|
| 116 |
+
P.crystal(ctx, cam(96, y + 40, 22, -0.6 + 1.1 * t + k), cell(r.e), {dark: th.dark});
|
| 117 |
+
label(sub(r.f), 168, y + 48, {size: 25, weight: 700});
|
| 118 |
+
label(fmt(r.v[0]), 330, y + 48, {size: 24, family: P.MATH}); label(fmt(r.v[1]), 510, y + 48, {size: 24, family: P.MATH});
|
| 119 |
+
ctx.fillStyle = th.line; ctx.fillRect(48, y + 79, 568, 1);
|
| 120 |
+
ctx.restore();
|
| 121 |
+
});
|
| 122 |
+
label("four real rows of the Perov-5 training set", 56, 466, {size: 14, color: th.faint, alpha: T.seg(t, 0.4, 0.6)});
|
| 123 |
+
// the whole table: one point per material
|
| 124 |
+
var c = cam(800, 220, 82, -0.5 + 0.9 * t, -0.3), n = Math.round(DATA_CLOUD.length * T.ease(T.seg(t, 0.38, 0.85)));
|
| 125 |
+
var pts = DATA_CLOUD.slice(0, n).map(function (p) { return [c.proj(p), p]; }).sort(function (a, b) { return a[0][2] - b[0][2]; });
|
| 126 |
+
pts.forEach(function (q) { var d = (q[0][2] + 1.3) / 2.6; dot(c, q[1], 0.045, P.css(P.mix(P.rgbOf(COL.data), [255, 255, 255], 0.5 * (1 - d))), 0.55 + 0.45 * d); });
|
| 127 |
+
label((Math.round(11356 * T.ease(T.seg(t, 0.4, 0.85)))).toLocaleString("en-US"), 800, 420, {size: 52, weight: 800, color: tc("data"), align: "center", alpha: T.seg(t, 0.38, 0.45)});
|
| 128 |
+
label("materials · 2 properties each", 800, 456, {size: 20, weight: 600, align: "center", color: th.muted, alpha: T.seg(t, 0.7, 0.88)});
|
| 129 |
+
}},
|
| 130 |
+
|
| 131 |
+
{block: "model", title: "Model", ms: 7800,
|
| 132 |
+
cap: "Two encoders place each crystal and its properties on one shared map. Training pulls the two points of the same material together.",
|
| 133 |
+
draw: function (t) {
|
| 134 |
+
var col = COL.model, a1 = T.ease(T.seg(t, 0.05, 0.2)), a2 = T.ease(T.seg(t, 0.18, 0.34));
|
| 135 |
+
P.crystal(ctx, cam(96, 150, 36, -0.5 + 0.8 * t), cell(ROWS[0].e), {dark: th.dark});
|
| 136 |
+
label("crystal", 96, 238, {size: 16, weight: 700, color: th.muted, align: "center"});
|
| 137 |
+
card(40, 296, 112, 70, {r: 12});
|
| 138 |
+
label("4.3 eV", 96, 326, {size: 21, weight: 800, align: "center"}); label("−0.24 eV/atom", 96, 350, {size: 13, color: th.muted, align: "center"});
|
| 139 |
+
label("properties", 96, 392, {size: 16, weight: 700, color: th.muted, align: "center"});
|
| 140 |
+
P.arrow(ctx, 158, 150, 206, 150, a1, col, 3.5); P.arrow(ctx, 158, 331, 206, 331, a1, col, 3.5);
|
| 141 |
+
[[115, "crystal encoder", "graph network"], [296, "property encoder", "dense network"]].forEach(function (b) {
|
| 142 |
+
card(210, b[0], 180, 70, {fill: th.dark ? "rgba(235,104,52,.14)" : "rgba(253,236,228,.95)", stroke: col, sw: 2.5});
|
| 143 |
+
label(b[1], 300, b[0] + 33, {size: 18, weight: 800, color: tc("model"), align: "center"});
|
| 144 |
+
label(b[2], 300, b[0] + 55, {size: 13, color: th.muted, align: "center", family: P.MONO});
|
| 145 |
+
});
|
| 146 |
+
P.arrow(ctx, 394, 150, 470, 205, a2, col, 3.5); P.arrow(ctx, 394, 331, 470, 290, a2, col, 3.5);
|
| 147 |
+
// the shared map as a 3D space
|
| 148 |
+
var c = cam(700, 262, 108, -0.65 + 0.55 * t, -0.32), box = 1.15, show = T.seg(t, 0.28, 0.4), pull = T.ease(T.seg(t, 0.42, 0.86));
|
| 149 |
+
ctx.save(); ctx.globalAlpha *= show;
|
| 150 |
+
[[-1, -1], [-1, 1], [1, -1], [1, 1]].forEach(function (u) {
|
| 151 |
+
P.line3(ctx, c, [-box, u[0] * box, u[1] * box], [box, u[0] * box, u[1] * box], th.dark ? "rgba(148,163,184,.35)" : "rgba(100,116,139,.35)", 1);
|
| 152 |
+
P.line3(ctx, c, [u[0] * box, -box, u[1] * box], [u[0] * box, box, u[1] * box], th.dark ? "rgba(148,163,184,.35)" : "rgba(100,116,139,.35)", 1);
|
| 153 |
+
P.line3(ctx, c, [u[0] * box, u[1] * box, -box], [u[0] * box, u[1] * box, box], th.dark ? "rgba(148,163,184,.35)" : "rgba(100,116,139,.35)", 1);
|
| 154 |
+
});
|
| 155 |
+
var R = T.rng(7), pairs = [];
|
| 156 |
+
for (var k = 0; k < 8; k++) {
|
| 157 |
+
var m = [R() * 1.6 - 0.8, R() * 1.6 - 0.8, R() * 1.6 - 0.8], d = [R() - 0.5, R() - 0.5, R() - 0.5], s = 0.55 * (1 - pull);
|
| 158 |
+
pairs.push({a: [m[0] + d[0] * s, m[1] + d[1] * s, m[2] + d[2] * s], b: [m[0] - d[0] * s, m[1] - d[1] * s, m[2] - d[2] * s]});
|
| 159 |
+
}
|
| 160 |
+
pairs.forEach(function (pr) { P.line3(ctx, c, pr.a, pr.b, th.dark ? "rgba(203,213,225,.55)" : "rgba(100,116,139,.6)", 1.4, [4, 4]); });
|
| 161 |
+
pairs.forEach(function (pr) {
|
| 162 |
+
P.sphere(ctx, c, pr.a, 0.07, COL.data, {dark: th.dark});
|
| 163 |
+
var q = c.proj(pr.b); ctx.beginPath(); ctx.arc(q[0], q[1], 0.08 * q[3], 0, 2 * Math.PI); ctx.lineWidth = 3; ctx.strokeStyle = COL.targets; ctx.stroke();
|
| 164 |
+
});
|
| 165 |
+
ctx.restore();
|
| 166 |
+
label("shared map (latent space)", 520, 84, {size: 17, weight: 800, color: th.muted, alpha: show});
|
| 167 |
+
label("crystal", 538, 474, {size: 15, color: th.muted, alpha: show}); dotFlat(526, 469, 6, COL.data, show);
|
| 168 |
+
ringFlat(612, 469, 7, COL.targets, show); label("its properties", 626, 474, {size: 15, color: th.muted, alpha: show});
|
| 169 |
+
label("pulled together", 930, 474, {size: 15, weight: 800, color: tc("model"), align: "right", alpha: T.seg(t, 0.8, 0.92)});
|
| 170 |
+
}},
|
| 171 |
+
|
| 172 |
+
{block: "family", title: "Family", ms: 7000,
|
| 173 |
+
cap: "You choose a crystal type, here the cubic ABX₃ halide perovskite, and the elements allowed on each site: 924 compositions.",
|
| 174 |
+
draw: function (t) {
|
| 175 |
+
var step = Math.min(5, Math.floor(t * 7)), combo = {};
|
| 176 |
+
GROUPS.forEach(function (g, j) { var vis = g.sample.slice(0, 6); combo[g.g] = vis[(step * (j + 2) + j) % vis.length]; });
|
| 177 |
+
P.crystal(ctx, cam(232, 262, 96, -0.9 + 1.25 * t), cell(combo), {dark: th.dark, labels: true});
|
| 178 |
+
label("cubic ABX₃ perovskite", 232, 478, {size: 21, weight: 800, color: tc("family"), align: "center"});
|
| 179 |
+
label(sub(combo.A + combo.B + combo.X + "3"), 232, 506, {size: 17, weight: 600, color: th.muted, align: "center"});
|
| 180 |
+
var x0 = 480, W = 450;
|
| 181 |
+
GROUPS.forEach(function (g, j) {
|
| 182 |
+
var x = x0 + W / 3 * (j + 0.5), a = T.out(T.seg(t, 0.05 + j * 0.08, 0.25 + j * 0.08));
|
| 183 |
+
chip(x, 84, g.g + " site", "family", true, {w: 104, h: 34, size: 18, alpha: a});
|
| 184 |
+
g.sample.slice(0, 6).forEach(function (el, m) { chip(x, 132 + m * 40, el, "family", el === combo[g.g], {w: 72, alpha: a}); });
|
| 185 |
+
if (g.sample.length > 6) label("+" + (g.sample.length - 6) + " more", x, 132 + 6 * 40 + 2, {size: 15, color: th.muted, align: "center", alpha: a});
|
| 186 |
+
});
|
| 187 |
+
label("21 × 11 × 4 = " + Math.round(924 * T.ease(T.seg(t, 0.42, 0.85))).toLocaleString("en-US") + " compositions", x0 + W / 2, 470,
|
| 188 |
+
{size: 26, weight: 800, color: tc("family"), align: "center", alpha: T.seg(t, 0.4, 0.48)});
|
| 189 |
+
}},
|
| 190 |
+
|
| 191 |
+
{block: "rules", title: "Rules", ms: 8000,
|
| 192 |
+
cap: "Simple chemistry rules remove impossible combinations: charges must balance and ions must fit. 27 of 924 pass every rule.",
|
| 193 |
+
draw: function (t) {
|
| 194 |
+
var c = cam(470, 300, 66, -0.42, -0.42), GX = [-2.6, -0.6, 1.4], EXIT = 3.9, START = -6.2;
|
| 195 |
+
label(Math.round(T.lerp(924, 27, T.ease(T.seg(t, 0.1, 0.92)))).toLocaleString("en-US"), 40, 96, {size: 40, weight: 800});
|
| 196 |
+
label("compositions left", 40, 122, {size: 15, color: th.muted});
|
| 197 |
+
// the belt
|
| 198 |
+
var shade = th.dark ? "rgba(148,163,184,.35)" : "rgba(100,116,139,.38)";
|
| 199 |
+
P.line3(ctx, c, [START, -0.75, -1.05], [EXIT + 0.4, -0.75, -1.05], shade, 1.5); P.line3(ctx, c, [START, -0.75, 1.05], [EXIT + 0.4, -0.75, 1.05], shade, 1.5);
|
| 200 |
+
for (var s = 0; s < 22; s++) { var x = START + ((s * 0.5 + t * 6) % 11); if (x < EXIT + 0.4) P.line3(ctx, c, [x, -0.75, -1.05], [x, -0.75, 1.05], shade, 1); }
|
| 201 |
+
// the gates: glass panes
|
| 202 |
+
GX.forEach(function (gx, j) {
|
| 203 |
+
var a = [gx, -0.75, -1.2], b = [gx, -0.75, 1.2], d = [gx, 1.45, 1.2], e = [gx, 1.45, -1.2];
|
| 204 |
+
var q = [a, b, d, e].map(c.proj);
|
| 205 |
+
ctx.save(); ctx.beginPath(); q.forEach(function (p, i) { i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]); }); ctx.closePath();
|
| 206 |
+
ctx.fillStyle = tint("rules", th.dark ? 0.16 : 0.2); ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = tint("rules", 0.85); ctx.stroke(); ctx.restore();
|
| 207 |
+
var top = c.proj([gx, 1.6, 0]), words = RULES[j].name.split(" ");
|
| 208 |
+
label(words[0], top[0], top[1] - 24, {size: 16, weight: 800, color: tc("rules"), align: "center"});
|
| 209 |
+
label(words.slice(1).join(" "), top[0], top[1] - 6, {size: 16, weight: 800, color: tc("rules"), align: "center"});
|
| 210 |
+
var bot = c.proj([gx, -0.75, 1.35]);
|
| 211 |
+
label("−" + RULES[j].lost, bot[0], bot[1] + 26, {size: 19, weight: 800, color: th.bad, align: "center", alpha: T.seg(t, 0.25 + j * 0.13, 0.35 + j * 0.13)});
|
| 212 |
+
});
|
| 213 |
+
var ex = c.proj([EXIT + 0.4, 1.6, 0]);
|
| 214 |
+
label("pass every", ex[0] + 26, ex[1] - 24, {size: 16, weight: 800, color: th.good, align: "center"});
|
| 215 |
+
label("rule", ex[0] + 26, ex[1] - 6, {size: 16, weight: 800, color: th.good, align: "center"});
|
| 216 |
+
var passed = 0, span = 0.36;
|
| 217 |
+
FLOW.forEach(function (it, k) {
|
| 218 |
+
var st = 0.03 + k * 0.085, stop = it.gate >= 0 ? GX[it.gate] - 0.55 : EXIT, tStop = st + span * (stop - START) / (EXIT - START);
|
| 219 |
+
if (t < st) return;
|
| 220 |
+
var lane = k % 2 ? 0.6 : -0.6, x = t < tStop ? T.lerp(START, EXIT, (t - st) / span) : stop, y = -0.25, alpha = 1, fall = 0;
|
| 221 |
+
if (t >= tStop && it.gate >= 0) { fall = T.ease(T.seg(t, tStop + 0.04, tStop + 0.13)); alpha = 1 - fall; y -= 1.4 * fall; }
|
| 222 |
+
if (t >= tStop && it.gate < 0) { var slot = passed++, f = T.ease(T.seg(t, tStop, tStop + 0.08));
|
| 223 |
+
var to = [870, 228 + slot * 70], from = c.proj([EXIT, y, lane]);
|
| 224 |
+
var cx2 = T.lerp(from[0], to[0], f), cy2 = T.lerp(from[1], to[1], f);
|
| 225 |
+
P.crystal(ctx, cam(cx2, cy2, 15, t * 3 + k, -0.35), cell(it.e), {dark: th.dark, glass: 0.8});
|
| 226 |
+
label(sub(it.f), cx2 + 24, cy2 + 6, {size: 16, weight: 800, alpha: f});
|
| 227 |
+
P.check(ctx, cx2 - 22, cy2 - 20, true, T.seg(t, tStop, tStop + 0.04), th);
|
| 228 |
+
return; }
|
| 229 |
+
if (alpha <= 0.02) return;
|
| 230 |
+
P.crystal(ctx, c, cell(it.e), {at: [x, y, lane], s: 0.32, spin: t * 3 + k, dark: th.dark, alpha: alpha, glass: 0.8});
|
| 231 |
+
var q = c.proj([x, y + 0.55, lane]);
|
| 232 |
+
label(sub(it.f), q[0], q[1] - 4, {size: 14, weight: 800, align: "center", alpha: alpha});
|
| 233 |
+
if (t >= tStop && it.gate >= 0) P.check(ctx, q[0] + 30, q[1] + 8, false, T.seg(t, tStop, tStop + 0.04), th);
|
| 234 |
+
});
|
| 235 |
+
}},
|
| 236 |
+
|
| 237 |
+
{block: "targets", title: "Targets", ms: 7400,
|
| 238 |
+
cap: "You say what you want, for example a direct band gap of 2 eV. The model predicts every composition and ranks the closest.",
|
| 239 |
+
draw: function (t) {
|
| 240 |
+
var c = cam(330, 282, 76, -0.32 + 0.18 * Math.sin(t * 2.2), -0.3);
|
| 241 |
+
var xl = -0.3, xh = 2.4, yl = 0.3, yh = 3.8, X = function (v) { return -2.4 + (v - xl) / (xh - xl) * 4.8; }, Y = function (v) { return -1.7 + (v - yl) / (yh - yl) * 3.4; };
|
| 242 |
+
var ax = th.dark ? "rgba(203,213,225,.6)" : "rgba(71,85,105,.6)", gr = th.dark ? "rgba(148,163,184,.14)" : "rgba(100,116,139,.15)";
|
| 243 |
+
for (var g = 0; g <= 4; g++) { P.line3(ctx, c, [-2.4 + g * 1.2, -1.7, 0], [-2.4 + g * 1.2, 1.7, 0], gr, 1); P.line3(ctx, c, [-2.4, -1.7 + g * 0.85, 0], [2.4, -1.7 + g * 0.85, 0], gr, 1); }
|
| 244 |
+
P.line3(ctx, c, [-2.4, -1.7, 0], [2.6, -1.7, 0], ax, 2); P.line3(ctx, c, [-2.4, -1.7, 0], [-2.4, 1.9, 0], ax, 2);
|
| 245 |
+
var xa = c.proj([0, -2.15, 0]), ya = c.proj([-2.85, 0, 0]);
|
| 246 |
+
label("predicted formation enthalpy (eV/atom)", xa[0], xa[1] + 10, {size: 14, color: th.muted, align: "center"});
|
| 247 |
+
ctx.save(); ctx.translate(ya[0] - 6, ya[1]); ctx.rotate(-Math.PI / 2); label("predicted direct band gap (eV)", 0, 0, {size: 14, color: th.muted, align: "center"}); ctx.restore();
|
| 248 |
+
var tg = [X(-0.1), Y(2.0), 0], tq = c.proj(tg), ta = T.seg(t, 0.16, 0.26);
|
| 249 |
+
ctx.save(); ctx.globalAlpha *= ta; ctx.setLineDash([6, 5]); ctx.strokeStyle = COL.targets; ctx.lineWidth = 2;
|
| 250 |
+
var v1 = c.proj([tg[0], -1.7, 0]), v2 = c.proj([tg[0], 1.7, 0]), h1 = c.proj([-2.4, tg[1], 0]), h2 = c.proj([2.4, tg[1], 0]);
|
| 251 |
+
ctx.beginPath(); ctx.moveTo(v1[0], v1[1]); ctx.lineTo(v2[0], v2[1]); ctx.moveTo(h1[0], h1[1]); ctx.lineTo(h2[0], h2[1]); ctx.stroke(); ctx.setLineDash([]);
|
| 252 |
+
ctx.beginPath(); ctx.arc(tq[0], tq[1], 15, 0, 2 * Math.PI); ctx.lineWidth = 3.5; ctx.stroke(); ctx.restore();
|
| 253 |
+
label("your target", tq[0] + 18, tq[1] + 36, {size: 16, weight: 800, color: tc("targets"), alpha: T.seg(t, 0.3, 0.42)});
|
| 254 |
+
var pts = ALIVE.map(function (r, k) { return {r: r, k: k, p: [X(r[1]), Y(r[2]), 0], rank: RANKED.indexOf(r[0])}; });
|
| 255 |
+
pts.forEach(function (o) { if (o.rank < 0) P.sphere(ctx, c, o.p, 0.075, COL.family, {dark: th.dark, alpha: T.seg(t, 0.02 + o.k * 0.006, 0.12 + o.k * 0.006) * 0.8}); });
|
| 256 |
+
pts.filter(function (o) { return o.rank >= 0; }).sort(function (a, b) { return b.rank - a.rank; }).forEach(function (o) {
|
| 257 |
+
var rise = T.ease(T.seg(t, 0.5 + o.rank * 0.07, 0.64 + o.rank * 0.07)), top = [o.p[0], o.p[1], 0.9 * rise];
|
| 258 |
+
P.line3(ctx, c, o.p, top, P.css(P.rgbOf(COL.candidates), 0.7), 2);
|
| 259 |
+
var q = P.sphere(ctx, c, top, 0.13, COL.candidates, {dark: th.dark, alpha: T.seg(t, 0.48, 0.52)});
|
| 260 |
+
label(String(o.rank + 1), q[0], q[1] + 5, {size: 13, weight: 800, color: "#fff", align: "center", alpha: rise});
|
| 261 |
+
});
|
| 262 |
+
var a = T.seg(t, 0.55, 0.68);
|
| 263 |
+
card(650, 84, 280, 330, {alpha: a});
|
| 264 |
+
label("closest to your target", 670, 118, {size: 17, weight: 800, color: tc("targets"), alpha: a});
|
| 265 |
+
RANKED.forEach(function (f, k) {
|
| 266 |
+
var r = ALIVE.filter(function (x) { return x[0] === f; })[0], y = 170 + k * 78, b = T.seg(t, 0.6 + k * 0.08, 0.7 + k * 0.08);
|
| 267 |
+
ctx.save(); ctx.globalAlpha *= b; ctx.beginPath(); ctx.arc(686, y - 7, 14, 0, 2 * Math.PI); ctx.fillStyle = COL.candidates; ctx.fill(); ctx.restore();
|
| 268 |
+
label(String(k + 1), 686, y - 1, {size: 15, weight: 800, color: "#fff", align: "center", alpha: b});
|
| 269 |
+
label(sub(f), 712, y, {size: 22, weight: 800, alpha: b});
|
| 270 |
+
label(fmt(r[2]) + " eV · " + fmt(r[1]) + " eV/atom", 712, y + 24, {size: 15, color: th.muted, alpha: b});
|
| 271 |
+
});
|
| 272 |
+
label("27 compositions that pass every rule", 670, 400, {size: 13, color: th.faint, alpha: a});
|
| 273 |
+
}},
|
| 274 |
+
|
| 275 |
+
{block: "search", title: "Search", ms: 8800,
|
| 276 |
+
cap: "Starting at your target’s point on the map, the search moves points until the model predicts your targets, turns each into a crystal and keeps those that pass every rule.",
|
| 277 |
+
draw: function (t) {
|
| 278 |
+
var c = cam(330, 280, 104, -0.4 + 0.5 * t, -0.3);
|
| 279 |
+
card(690, 64, 240, 412);
|
| 280 |
+
label("found", 712, 98, {size: 20, weight: 800, color: tc("search")}); label("pass every rule", 712, 120, {size: 15, color: th.muted});
|
| 281 |
+
MAP_CLOUD.map(function (p) { return [c.proj(p), p]; }).sort(function (a, b) { return a[0][2] - b[0][2]; })
|
| 282 |
+
.forEach(function (q) { dot(c, q[1], 0.03, th.dark ? "#94a3b8" : "#94a3b8", 0.25 + 0.25 * (q[0][2] + 1.7) / 3.4); });
|
| 283 |
+
var o = c.proj([0, 0, 0]);
|
| 284 |
+
star(o[0], o[1], 16, COL.targets);
|
| 285 |
+
label("your target", o[0], o[1] + 36, {size: 15, weight: 800, color: tc("targets"), align: "center"});
|
| 286 |
+
var who = [{ok: false, e: STOPPED[0].e}, {ok: true, i: 0}, {ok: false, e: STOPPED[1].e}, {ok: false, e: STOPPED[2].e},
|
| 287 |
+
{ok: true, i: 1}, {ok: false, e: STOPPED[3].e}, {ok: true, i: 2}, {ok: false, e: STOPPED[4].e}];
|
| 288 |
+
function pos(k, u) {
|
| 289 |
+
var th0 = k * 0.785 + 0.3, ph = 0.6 * Math.sin(k * 1.7), r = 0.18 + 1.25 * T.ease(T.seg(u, 0.06, 0.58)) * (0.6 + 0.4 * ((k * 37) % 10) / 10);
|
| 290 |
+
return [r * Math.cos(th0 + 0.8 * u) * Math.cos(ph), r * Math.sin(ph) + 0.08 * Math.sin(u * 9 + k), r * Math.sin(th0 + 0.8 * u) * Math.cos(ph)];
|
| 291 |
+
}
|
| 292 |
+
var slot = 0;
|
| 293 |
+
who.forEach(function (w, k) {
|
| 294 |
+
var p = pos(k, Math.min(t, 0.6));
|
| 295 |
+
if (t < 0.6) {
|
| 296 |
+
ctx.save(); ctx.strokeStyle = P.css(P.rgbOf(COL.search), 0.5); ctx.lineWidth = 2.5; ctx.beginPath();
|
| 297 |
+
for (var q = 8; q >= 0; q--) { var pp = c.proj(pos(k, Math.max(0, t - q * 0.015))); q === 8 ? ctx.moveTo(pp[0], pp[1]) : ctx.lineTo(pp[0], pp[1]); }
|
| 298 |
+
ctx.stroke(); ctx.restore();
|
| 299 |
+
P.sphere(ctx, c, p, 0.07, th.dark ? "#22c55e" : COL.search, {dark: th.dark, alpha: T.seg(t, 0.03, 0.1)});
|
| 300 |
+
return;
|
| 301 |
+
}
|
| 302 |
+
var pop = T.pop(T.seg(t, 0.6, 0.68)), chk = T.seg(t, 0.7, 0.76), fade = w.ok ? 0 : T.seg(t, 0.8, 0.88), fly = w.ok ? T.ease(T.seg(t, 0.84, 0.97)) : 0;
|
| 303 |
+
var my = w.ok ? slot++ : 0, q = c.proj(p), X = T.lerp(q[0], 740, fly), Y = T.lerp(q[1], 190 + my * 92, fly);
|
| 304 |
+
var e = w.ok ? FOUND[w.i].e : w.e;
|
| 305 |
+
if (fade >= 1) return;
|
| 306 |
+
ctx.save(); ctx.globalAlpha *= 1 - fade;
|
| 307 |
+
P.crystal(ctx, cam(X, Y, 19 * Math.max(0.05, pop), -0.6 + t * 2, -0.35), cell(e), {dark: th.dark, glass: 0.8});
|
| 308 |
+
if (chk > 0) P.check(ctx, X + 24, Y - 24, w.ok, chk, th);
|
| 309 |
+
ctx.restore();
|
| 310 |
+
if (w.ok) label(sub(FOUND[w.i].f), 782, 197 + my * 92, {size: 21, weight: 800, alpha: T.seg(t, 0.92, 0.99)});
|
| 311 |
+
});
|
| 312 |
+
}},
|
| 313 |
+
|
| 314 |
+
{block: "candidates", title: "Candidates", ms: 7800,
|
| 315 |
+
cap: "Each new material comes with its crystal, the rules it passed and its predicted properties. Confirm the best ones with calculations or experiments.",
|
| 316 |
+
draw: function (t) {
|
| 317 |
+
label("example: a search with the Studio defaults", 930, 52, {size: 13, weight: 700, color: th.muted, align: "right", alpha: T.seg(t, 0.1, 0.25)});
|
| 318 |
+
FOUND.forEach(function (f, k) {
|
| 319 |
+
var x0 = 34 + k * 302, a = T.out(T.seg(t, 0.04 + k * 0.12, 0.3 + k * 0.12));
|
| 320 |
+
ctx.save(); ctx.translate(0, 50 * (1 - a)); ctx.globalAlpha *= a;
|
| 321 |
+
card(x0, 70, 286, 330, {stroke: COL.candidates, sw: 2.2});
|
| 322 |
+
P.crystal(ctx, cam(x0 + 70, 146, 34, -0.6 + 0.8 * t + k), cell(f.e), {dark: th.dark});
|
| 323 |
+
label(sub(f.f), x0 + 134, 132, {size: 25, weight: 800});
|
| 324 |
+
label("✓ every rule", x0 + 134, 158, {size: 15, weight: 700, color: th.good});
|
| 325 |
+
if (f.flag) label("⚠ extrapolating", x0 + 134, 180, {size: 13, weight: 700, color: th.warn});
|
| 326 |
+
[["Direct band gap", f.gap, 2.0, "eV", [0, 7.9]], ["Formation enthalpy", f.dh, -0.1, "eV/atom", [-0.64, 5.16]]].forEach(function (b, j) {
|
| 327 |
+
var y = 232 + j * 82, lo = Math.min(b[4][0], b[1], b[2]), hi = Math.max(b[4][1], b[1], b[2]), G = function (v) { return x0 + 22 + (v - lo) / ((hi - lo) || 1) * 242; };
|
| 328 |
+
label(b[0], x0 + 22, y, {size: 14, color: th.muted});
|
| 329 |
+
label(fmt(b[1]) + " " + b[3], x0 + 22, y + 22, {size: 17, weight: 800});
|
| 330 |
+
ctx.fillStyle = th.line; P.rrect(ctx, x0 + 22, y + 32, 242, 7, 3.5); ctx.fill();
|
| 331 |
+
ctx.save(); ctx.setLineDash([3, 2]); ctx.strokeStyle = COL.targets; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(G(b[2]), y + 27); ctx.lineTo(G(b[2]), y + 44); ctx.stroke(); ctx.restore();
|
| 332 |
+
label("target " + fmt(b[2]), T.clamp(G(b[2]), x0 + 50, x0 + 236), y + 60, {size: 12, weight: 700, color: tc("targets"), align: "center"});
|
| 333 |
+
ctx.beginPath(); ctx.arc(G(b[1]), y + 35.5, 6.5, 0, 2 * Math.PI); ctx.fillStyle = COL.candidates; ctx.fill();
|
| 334 |
+
});
|
| 335 |
+
ctx.restore();
|
| 336 |
+
});
|
| 337 |
+
var a2 = T.seg(t, 0.62, 0.74);
|
| 338 |
+
card(245, 428, 470, 42, {r: 21, fill: COL.candidates, stroke: null, alpha: a2});
|
| 339 |
+
label("next: confirm with DFT or an experiment", 480, 455, {size: 19, weight: 700, color: "#fff", align: "center", alpha: a2});
|
| 340 |
+
}},
|
| 341 |
+
|
| 342 |
+
{block: "outro", title: "Your turn", ms: 4600,
|
| 343 |
+
cap: "Your turn: open the Studio, change any block and watch the effect flow through the next ones. A good start: Targets, then Search.",
|
| 344 |
+
draw: function (t) {
|
| 345 |
+
P.crystal(ctx, cam(480, 250, 120, -0.6 + 0.9 * t, -0.35), cell(FOUND[0].e), {dark: th.dark, alpha: th.dark ? 0.07 : 0.1, glass: 0.6});
|
| 346 |
+
var a = T.out(T.seg(t, 0, 0.25));
|
| 347 |
+
label("Your turn", 480, 178, {size: 54, weight: 800, align: "center", alpha: a});
|
| 348 |
+
label("Open the Studio and change any block:", 480, 226, {size: 23, color: th.muted, align: "center", alpha: T.seg(t, 0.15, 0.35)});
|
| 349 |
+
label("the effect flows to the blocks after it.", 480, 258, {size: 23, color: th.muted, align: "center", alpha: T.seg(t, 0.2, 0.4)});
|
| 350 |
+
var wave = (t * 2.2) % 1;
|
| 351 |
+
BLOCKS.forEach(function (b, k) {
|
| 352 |
+
var x = 165 + k * 105, on = Math.abs(wave * 7 - k - 0.5) < 0.8, al = T.seg(t, 0.25 + k * 0.03, 0.35 + k * 0.03);
|
| 353 |
+
if (k < 6) { ctx.save(); ctx.globalAlpha *= T.seg(t, 0.3, 0.4); ctx.strokeStyle = th.faint; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(x + 20, 330); ctx.lineTo(x + 85, 330); ctx.stroke(); ctx.restore(); }
|
| 354 |
+
ctx.save(); ctx.globalAlpha *= al; ctx.beginPath(); ctx.arc(x, 330, on ? 17 : 12, 0, 2 * Math.PI); ctx.fillStyle = b.c; ctx.shadowColor = b.c; ctx.shadowBlur = on ? 14 : 0; ctx.fill(); ctx.restore();
|
| 355 |
+
label(b.name, x, 372, {size: 14, weight: 700, color: tc(b.id), align: "center", alpha: al});
|
| 356 |
+
});
|
| 357 |
+
}}
|
| 358 |
+
];
|
| 359 |
+
function dotFlat(x, y, r, color, a) { ctx.save(); ctx.globalAlpha *= a; ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); ctx.restore(); }
|
| 360 |
+
function ringFlat(x, y, r, color, a) { ctx.save(); ctx.globalAlpha *= a; ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.lineWidth = 2.5; ctx.strokeStyle = color; ctx.stroke(); ctx.restore(); }
|
| 361 |
+
|
| 362 |
+
/* ── player ── */
|
| 363 |
+
var TOTAL = SCENES.reduce(function (a, s) { return a + s.ms; }, 0);
|
| 364 |
+
var S = {ms: 0, playing: false, last: 0, raf: 0, i: -1, visible: false, started: false, W: 0, H: 0, dpr: 1};
|
| 365 |
+
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 366 |
+
var $ = function (sel) { return root.querySelector(sel); };
|
| 367 |
+
var strip = $(".t3-strip"), cap = $(".t3-cap"), prog = $(".t3-prog"), playBtn = $("[data-t3=play]"), open = $(".t3-open"), time = $(".t3-time");
|
| 368 |
+
prog.setAttribute("aria-valuemax", String(Math.round(TOTAL / 1000)));
|
| 369 |
+
strip.innerHTML = BLOCKS.map(function (b) {
|
| 370 |
+
return '<li><button type="button" data-block="' + b.id + '" style="--bc:' + b.c + '"><span>' + b.name + "</span><i></i></button></li>";
|
| 371 |
+
}).join("");
|
| 372 |
+
function at(ms) { var a = 0; for (var i = 0; i < SCENES.length; i++) { if (ms < a + SCENES[i].ms || i === SCENES.length - 1) return {i: i, t: T.clamp((ms - a) / SCENES[i].ms), start: a}; a += SCENES[i].ms; } }
|
| 373 |
+
function startOf(i) { var a = 0; for (var k = 0; k < i; k++) a += SCENES[k].ms; return a; }
|
| 374 |
+
function clock(ms) { var s = Math.round(ms / 1000); return Math.floor(s / 60) + ":" + ("0" + (s % 60)).slice(-2); }
|
| 375 |
+
function resize() {
|
| 376 |
+
var r = canvas.getBoundingClientRect();
|
| 377 |
+
S.dpr = Math.min(2, window.devicePixelRatio || 1); S.W = Math.max(1, r.width); S.H = Math.max(1, r.height);
|
| 378 |
+
canvas.width = Math.round(S.W * S.dpr); canvas.height = Math.round(S.H * S.dpr);
|
| 379 |
+
draw();
|
| 380 |
+
}
|
| 381 |
+
function draw() {
|
| 382 |
+
if (!S.W) return;
|
| 383 |
+
th = P.theme();
|
| 384 |
+
var w = at(S.ms), sc = SCENES[w.i], k = S.W / VW;
|
| 385 |
+
ctx.setTransform(S.dpr, 0, 0, S.dpr, 0, 0); ctx.clearRect(0, 0, S.W, S.H);
|
| 386 |
+
ctx.setTransform(S.dpr * k, 0, 0, S.dpr * k, 0, (S.H - VH * k) / 2 * S.dpr);
|
| 387 |
+
try { sc.draw(w.t); } catch (e) { if (window.console) console.error(e); }
|
| 388 |
+
prog.firstElementChild.style.width = (100 * S.ms / TOTAL).toFixed(2) + "%";
|
| 389 |
+
prog.setAttribute("aria-valuenow", String(Math.round(S.ms / 1000)));
|
| 390 |
+
time.textContent = clock(S.ms) + " / " + clock(TOTAL);
|
| 391 |
+
var bi = BLOCKS.map(function (b) { return b.id; }).indexOf(sc.block);
|
| 392 |
+
strip.querySelectorAll("button").forEach(function (b, j) {
|
| 393 |
+
b.classList.toggle("on", j === bi); b.classList.toggle("done", (bi >= 0 && j < bi) || sc.block === "outro");
|
| 394 |
+
b.querySelector("i").style.width = j === bi ? (100 * w.t).toFixed(1) + "%" : "0";
|
| 395 |
+
});
|
| 396 |
+
if (w.i !== S.i) {
|
| 397 |
+
S.i = w.i;
|
| 398 |
+
var blk = BLOCKS[bi];
|
| 399 |
+
cap.innerHTML = (blk ? '<b style="color:' + tc(blk.id) + '">' + blk.name + ".</b> " : "") + sc.cap;
|
| 400 |
+
root.style.setProperty("--t3c", blk ? blk.c : COL.family);
|
| 401 |
+
open.href = blk ? "/studio/?panel=" + blk.id : "/studio/";
|
| 402 |
+
open.textContent = blk ? "Open " + blk.name + " in the Studio →" : "Open the Studio →";
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
function loop(now) {
|
| 406 |
+
S.raf = 0; if (!S.playing) return;
|
| 407 |
+
var dt = S.last ? Math.min(120, now - S.last) : 0; S.last = now;
|
| 408 |
+
if (S.visible && !document.hidden) {
|
| 409 |
+
S.ms = Math.min(TOTAL, S.ms + dt); draw();
|
| 410 |
+
if (S.ms >= TOTAL) { pause(); playBtn.textContent = "↺ Replay"; playBtn.setAttribute("aria-label", "Play the tour again"); return; }
|
| 411 |
+
}
|
| 412 |
+
S.raf = requestAnimationFrame(loop);
|
| 413 |
+
}
|
| 414 |
+
function play() {
|
| 415 |
+
if (S.ms >= TOTAL - 1) S.ms = 0;
|
| 416 |
+
S.playing = true; S.last = 0; S.started = true;
|
| 417 |
+
playBtn.textContent = "❚❚ Pause"; playBtn.setAttribute("aria-label", "Pause the tour");
|
| 418 |
+
if (!S.raf) S.raf = requestAnimationFrame(loop);
|
| 419 |
+
}
|
| 420 |
+
function pause() { S.playing = false; playBtn.textContent = "▶ Play"; playBtn.setAttribute("aria-label", "Play the tour"); }
|
| 421 |
+
function go(i) { // a chapter: from its start while playing, its finished picture while paused
|
| 422 |
+
i = Math.max(0, Math.min(SCENES.length - 1, i));
|
| 423 |
+
S.ms = startOf(i) + (S.playing ? 0 : SCENES[i].ms - 1); S.i = -1; draw();
|
| 424 |
+
}
|
| 425 |
+
playBtn.addEventListener("click", function () { S.playing ? pause() : play(); });
|
| 426 |
+
$("[data-t3=prev]").addEventListener("click", function () { go(at(S.ms).i - 1); });
|
| 427 |
+
$("[data-t3=next]").addEventListener("click", function () { go(at(S.ms).i + 1); });
|
| 428 |
+
strip.addEventListener("click", function (e) {
|
| 429 |
+
var b = e.target.closest && e.target.closest("[data-block]"); if (!b) return;
|
| 430 |
+
var i = SCENES.map(function (s) { return s.block; }).indexOf(b.getAttribute("data-block")); if (i >= 0) go(i);
|
| 431 |
+
});
|
| 432 |
+
function seek(e) { var r = prog.getBoundingClientRect(); S.ms = T.clamp((e.clientX - r.left) / r.width) * TOTAL; S.i = -1; draw(); }
|
| 433 |
+
prog.addEventListener("click", seek);
|
| 434 |
+
prog.addEventListener("keydown", function (e) {
|
| 435 |
+
if (e.key === "ArrowRight") { S.ms = Math.min(TOTAL, S.ms + 5000); draw(); e.preventDefault(); }
|
| 436 |
+
else if (e.key === "ArrowLeft") { S.ms = Math.max(0, S.ms - 5000); draw(); e.preventDefault(); }
|
| 437 |
+
});
|
| 438 |
+
root.addEventListener("keydown", function (e) {
|
| 439 |
+
if (e.target.closest && e.target.closest(".t3-prog")) return;
|
| 440 |
+
if (e.key === "k" || (e.key === " " && e.target === canvas)) { e.preventDefault(); S.playing ? pause() : play(); }
|
| 441 |
+
});
|
| 442 |
+
var th0 = document.getElementById("theme");
|
| 443 |
+
if (th0) th0.addEventListener("click", function () { setTimeout(function () { S.i = -1; draw(); }, 0); });
|
| 444 |
+
if (window.ResizeObserver) new ResizeObserver(resize).observe(canvas); else window.addEventListener("resize", resize);
|
| 445 |
+
if (window.IntersectionObserver) new IntersectionObserver(function (es) {
|
| 446 |
+
S.visible = es[0].isIntersecting;
|
| 447 |
+
if (S.visible && !S.started && !reduce) play(); // starts the first time it is seen
|
| 448 |
+
}, {threshold: 0.35}).observe(canvas);
|
| 449 |
+
else { S.visible = true; if (!reduce) play(); }
|
| 450 |
+
if (reduce) S.ms = SCENES[0].ms - 1; // still pictures; Play animates
|
| 451 |
+
resize();
|
| 452 |
+
window.prismTour = {play: play, pause: pause, go: go, seek: function (ms) { S.ms = T.clamp(ms, 0, TOTAL); S.i = -1; draw(); },
|
| 453 |
+
scenes: SCENES.map(function (s) { return {block: s.block, title: s.title, ms: s.ms}; })};
|
| 454 |
+
})();
|
site/assets/prism3d.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Prism3D: the small 3D toolkit of the MEIDNet Prism landing page (hero scene and the live tour).
|
| 2 |
+
A perspective camera, glossy atoms, glass faces and the cubic ABX3 perovskite cell, drawn on a 2D canvas with the
|
| 3 |
+
painter's algorithm. No libraries. */
|
| 4 |
+
(function () {
|
| 5 |
+
"use strict";
|
| 6 |
+
var FONT = "'Plus Jakarta Sans', system-ui, 'Segoe UI', sans-serif";
|
| 7 |
+
var MONO = "'IBM Plex Mono', ui-monospace, monospace";
|
| 8 |
+
var MATH = "'STIX Two Text', 'Cambria Math', serif";
|
| 9 |
+
var SITE = {A: "#8b5cf6", B: "#3b82f6", X: "#14b8a6"}; // atoms are coloured by site, labelled by element
|
| 10 |
+
var RADIUS = {A: 0.31, B: 0.29, X: 0.23};
|
| 11 |
+
|
| 12 |
+
function isDark() {
|
| 13 |
+
var t = document.documentElement.getAttribute("data-theme");
|
| 14 |
+
if (t) return t === "dark";
|
| 15 |
+
return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
| 16 |
+
}
|
| 17 |
+
function theme() {
|
| 18 |
+
var cs = getComputedStyle(document.documentElement), v = function (n, d) { return (cs.getPropertyValue(n) || "").trim() || d; };
|
| 19 |
+
var dark = isDark();
|
| 20 |
+
return {dark: dark, bg: v("--bg", dark ? "#111417" : "#f6f7f5"), card: v("--card", dark ? "#1b1f24" : "#fff"),
|
| 21 |
+
ink: v("--ink", dark ? "#fff" : "#0b0b0b"), muted: v("--muted", dark ? "#c3c2b7" : "#52514e"),
|
| 22 |
+
faint: v("--faint", "#898781"), line: v("--line", dark ? "#2c2c2a" : "#e1e0d9"),
|
| 23 |
+
good: dark ? "#4ade80" : "#16a34a", bad: dark ? "#f87171" : "#dc2626", warn: dark ? "#fbbf24" : "#b45309"};
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/* colours */
|
| 27 |
+
function rgbOf(hex) { var h = hex.replace("#", ""); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; }
|
| 28 |
+
function mix(a, b, t) { return a.map(function (v, i) { return Math.round(v + (b[i] - v) * t); }); }
|
| 29 |
+
function css(c, a) { return a === undefined ? "rgb(" + c.join(",") + ")" : "rgba(" + c.join(",") + "," + a + ")"; }
|
| 30 |
+
|
| 31 |
+
/* timing */
|
| 32 |
+
var T = {
|
| 33 |
+
clamp: function (x, a, b) { a = a === undefined ? 0 : a; b = b === undefined ? 1 : b; return Math.max(a, Math.min(b, x)); },
|
| 34 |
+
seg: function (t, a, b) { return Math.max(0, Math.min(1, (t - a) / (b - a))); },
|
| 35 |
+
ease: function (x) { x = Math.max(0, Math.min(1, x)); return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; },
|
| 36 |
+
out: function (x) { x = Math.max(0, Math.min(1, x)); return 1 - Math.pow(1 - x, 3); },
|
| 37 |
+
pop: function (x) { x = Math.max(0, Math.min(1, x)); var c = 1.70158; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); },
|
| 38 |
+
lerp: function (a, b, x) { return a + (b - a) * x; },
|
| 39 |
+
rng: function (seed) { return function () { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; }; }
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
/* a perspective camera: screen centre (cx, cy), pixels per world unit at the origin, yaw about y, pitch about x */
|
| 43 |
+
function camera(o) {
|
| 44 |
+
var cy = Math.cos(o.yaw || 0), sy = Math.sin(o.yaw || 0), cp = Math.cos(o.pitch || 0), sp = Math.sin(o.pitch || 0);
|
| 45 |
+
var D = o.dist || 9, F = o.scale * D;
|
| 46 |
+
function view(p) { var x1 = p[0] * cy + p[2] * sy, z1 = -p[0] * sy + p[2] * cy; return [x1, p[1] * cp - z1 * sp, p[1] * sp + z1 * cp]; }
|
| 47 |
+
return {
|
| 48 |
+
view: view,
|
| 49 |
+
proj: function (p) { var v = view(p), k = F / (D - v[2]); return [o.cx + v[0] * k, o.cy - v[1] * k, v[2], k]; }
|
| 50 |
+
};
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* drawing */
|
| 54 |
+
function sphere(ctx, cam, p, r, hex, o) {
|
| 55 |
+
o = o || {};
|
| 56 |
+
var q = cam.proj(p), R = r * q[3], c = rgbOf(hex), dark = o.dark;
|
| 57 |
+
if (R < 0.4) return q;
|
| 58 |
+
ctx.save();
|
| 59 |
+
if (o.alpha !== undefined) ctx.globalAlpha *= o.alpha;
|
| 60 |
+
var g = ctx.createRadialGradient(q[0] - R * 0.38, q[1] - R * 0.42, R * 0.06, q[0], q[1], R);
|
| 61 |
+
g.addColorStop(0, "rgba(255,255,255,.96)"); g.addColorStop(0.24, css(mix(c, [255, 255, 255], 0.45)));
|
| 62 |
+
g.addColorStop(0.72, css(c)); g.addColorStop(1, css(mix(c, [0, 0, 0], 0.5)));
|
| 63 |
+
ctx.beginPath(); ctx.arc(q[0], q[1], R, 0, 2 * Math.PI); ctx.fillStyle = g; ctx.fill();
|
| 64 |
+
ctx.lineWidth = Math.max(0.5, R * 0.04); ctx.strokeStyle = dark ? "rgba(0,0,0,.35)" : "rgba(15,23,42,.14)"; ctx.stroke();
|
| 65 |
+
if (o.label && R > 6.5) {
|
| 66 |
+
ctx.font = "800 " + Math.min(22, R * 0.82).toFixed(1) + "px " + FONT;
|
| 67 |
+
ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
| 68 |
+
ctx.lineWidth = Math.max(1.5, R * 0.12); ctx.strokeStyle = "rgba(15,23,42,.45)"; ctx.strokeText(o.label, q[0], q[1] + 0.5);
|
| 69 |
+
ctx.fillStyle = "#fff"; ctx.fillText(o.label, q[0], q[1] + 0.5);
|
| 70 |
+
}
|
| 71 |
+
ctx.restore();
|
| 72 |
+
return q;
|
| 73 |
+
}
|
| 74 |
+
var LIGHT = [-0.45, 0.7, 0.55];
|
| 75 |
+
function glass(ctx, cam, tri, o) {
|
| 76 |
+
o = o || {};
|
| 77 |
+
var q = tri.map(cam.proj), v = tri.map(cam.view);
|
| 78 |
+
var e1 = [v[1][0] - v[0][0], v[1][1] - v[0][1], v[1][2] - v[0][2]], e2 = [v[2][0] - v[0][0], v[2][1] - v[0][1], v[2][2] - v[0][2]];
|
| 79 |
+
var n = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]], l = Math.hypot(n[0], n[1], n[2]) || 1;
|
| 80 |
+
var lit = Math.abs((n[0] * LIGHT[0] + n[1] * LIGHT[1] + n[2] * LIGHT[2]) / l), a = o.alpha === undefined ? 1 : o.alpha, dark = o.dark;
|
| 81 |
+
var tint = rgbOf(o.tint || "#81a1fa");
|
| 82 |
+
ctx.beginPath(); ctx.moveTo(q[0][0], q[0][1]); ctx.lineTo(q[1][0], q[1][1]); ctx.lineTo(q[2][0], q[2][1]); ctx.closePath();
|
| 83 |
+
var g = ctx.createLinearGradient(q[0][0], q[0][1], q[2][0], q[2][1]);
|
| 84 |
+
g.addColorStop(0, dark ? css(mix(tint, [255, 255, 255], 0.35), (0.1 + 0.22 * lit) * a) : css([255, 255, 255], (0.25 + 0.45 * lit) * a));
|
| 85 |
+
g.addColorStop(1, css(tint, (dark ? 0.08 + 0.16 * lit : 0.12 + 0.24 * lit) * a));
|
| 86 |
+
ctx.fillStyle = g; ctx.fill();
|
| 87 |
+
ctx.lineWidth = 1; ctx.strokeStyle = dark ? "rgba(191,219,254," + 0.5 * a + ")" : "rgba(255,255,255," + 0.95 * a + ")"; ctx.stroke();
|
| 88 |
+
ctx.lineWidth = 0.6; ctx.strokeStyle = dark ? "rgba(96,165,250," + 0.35 * a + ")" : "rgba(99,102,241," + 0.25 * a + ")"; ctx.stroke();
|
| 89 |
+
}
|
| 90 |
+
function line3(ctx, cam, a, b, color, width, dash) {
|
| 91 |
+
var p = cam.proj(a), q = cam.proj(b);
|
| 92 |
+
ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = width || 1; if (dash) ctx.setLineDash(dash);
|
| 93 |
+
ctx.beginPath(); ctx.moveTo(p[0], p[1]); ctx.lineTo(q[0], q[1]); ctx.stroke(); ctx.restore();
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* the cubic ABX3 cell, half edge 1: A on the corners, B in the centre, X on the face centres.
|
| 97 |
+
el = {A: 'Na', B: 'Ni', X: 'I'}; X may be a list of three (the anions on the x, y and z faces). */
|
| 98 |
+
function perovskite(el) {
|
| 99 |
+
var X = Array.isArray(el.X) ? el.X : [el.X, el.X, el.X], atoms = [], edges = [], faces = [];
|
| 100 |
+
[-1, 1].forEach(function (x) { [-1, 1].forEach(function (y) { [-1, 1].forEach(function (z) { atoms.push({p: [x, y, z], site: "A", el: el.A}); }); }); });
|
| 101 |
+
atoms.push({p: [0, 0, 0], site: "B", el: el.B});
|
| 102 |
+
var xs = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
| 103 |
+
xs.forEach(function (p, i) { atoms.push({p: p, site: "X", el: X[Math.floor(i / 2)]}); });
|
| 104 |
+
var corners = atoms.slice(0, 8);
|
| 105 |
+
corners.forEach(function (a, i) { corners.forEach(function (b, j) {
|
| 106 |
+
if (j > i && Math.abs(a.p[0] - b.p[0]) + Math.abs(a.p[1] - b.p[1]) + Math.abs(a.p[2] - b.p[2]) === 2) edges.push([a.p, b.p]);
|
| 107 |
+
}); });
|
| 108 |
+
[[0, 2, 4], [0, 4, 3], [0, 3, 5], [0, 5, 2], [1, 4, 2], [1, 3, 4], [1, 5, 3], [1, 2, 5]].forEach(function (f) {
|
| 109 |
+
faces.push([xs[f[0]], xs[f[1]], xs[f[2]]]);
|
| 110 |
+
});
|
| 111 |
+
return {atoms: atoms, edges: edges, faces: faces};
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* a crystal at `at`, size `s` (world units per half edge), turned by `spin` about its own vertical axis.
|
| 115 |
+
o.explode (0..1) scatters the atoms (assembly animations), o.glass sets the octahedron's opacity, o.labels names atoms. */
|
| 116 |
+
function crystal(ctx, cam, cell, o) {
|
| 117 |
+
o = o || {};
|
| 118 |
+
var s = o.s || 1, at = o.at || [0, 0, 0], cs = Math.cos(o.spin || 0), sn = Math.sin(o.spin || 0), ex = o.explode || 0;
|
| 119 |
+
var R = T.rng(o.seed || 3);
|
| 120 |
+
function place(p, i) {
|
| 121 |
+
var x = p[0] * cs + p[2] * sn, z = -p[0] * sn + p[2] * cs, y = p[1];
|
| 122 |
+
if (ex) { var j = [R() - 0.5, R() - 0.5, R() - 0.5]; x = x * (1 + 1.6 * ex) + j[0] * 3 * ex; y = y * (1 + 1.6 * ex) + j[1] * 3 * ex; z = z * (1 + 1.6 * ex) + j[2] * 3 * ex; }
|
| 123 |
+
return [at[0] + x * s, at[1] + y * s, at[2] + z * s];
|
| 124 |
+
}
|
| 125 |
+
var items = [], ga = o.glass === undefined ? 1 : o.glass, ea = o.edges === undefined ? 1 : o.edges, placed = {};
|
| 126 |
+
var key = function (p) { return p.join(","); }, at_ = function (p) { return placed[key(p)]; }; // faces and edges follow their atoms
|
| 127 |
+
cell.atoms.forEach(function (a, i) {
|
| 128 |
+
var p = place(a.p, i);
|
| 129 |
+
placed[key(a.p)] = p;
|
| 130 |
+
items.push({z: cam.proj(p)[2], atom: a, p: p});
|
| 131 |
+
});
|
| 132 |
+
if (ga > 0.01) cell.faces.forEach(function (f) {
|
| 133 |
+
var tri = f.map(at_);
|
| 134 |
+
items.push({z: (cam.proj(tri[0])[2] + cam.proj(tri[1])[2] + cam.proj(tri[2])[2]) / 3, tri: tri});
|
| 135 |
+
});
|
| 136 |
+
if (ea > 0.01) cell.edges.forEach(function (e) {
|
| 137 |
+
var a = at_(e[0]), b = at_(e[1]);
|
| 138 |
+
items.push({z: (cam.proj(a)[2] + cam.proj(b)[2]) / 2 - 0.3 * s, edge: [a, b]});
|
| 139 |
+
});
|
| 140 |
+
items.sort(function (m, n) { return m.z - n.z; });
|
| 141 |
+
var alpha = o.alpha === undefined ? 1 : o.alpha;
|
| 142 |
+
ctx.save(); ctx.globalAlpha *= alpha;
|
| 143 |
+
items.forEach(function (it) {
|
| 144 |
+
if (it.atom) sphere(ctx, cam, it.p, RADIUS[it.atom.site] * s * (o.atomScale || 1), (o.colors && o.colors[it.atom.site]) || SITE[it.atom.site],
|
| 145 |
+
{dark: o.dark, label: o.labels ? it.atom.el : null});
|
| 146 |
+
else if (it.tri) glass(ctx, cam, it.tri, {dark: o.dark, alpha: ga});
|
| 147 |
+
else line3(ctx, cam, it.edge[0], it.edge[1], o.dark ? "rgba(148,163,184," + 0.5 * ea + ")" : "rgba(100,116,139," + 0.5 * ea + ")", 1.1);
|
| 148 |
+
});
|
| 149 |
+
ctx.restore();
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* flat pieces */
|
| 153 |
+
function rrect(ctx, x, y, w, h, r) {
|
| 154 |
+
r = Math.min(r, w / 2, h / 2);
|
| 155 |
+
ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
|
| 156 |
+
ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
|
| 157 |
+
}
|
| 158 |
+
function text(ctx, str, x, y, o) {
|
| 159 |
+
o = o || {};
|
| 160 |
+
ctx.save();
|
| 161 |
+
if (o.alpha !== undefined) ctx.globalAlpha *= T.clamp(o.alpha);
|
| 162 |
+
ctx.font = (o.italic ? "italic " : "") + (o.weight || 600) + " " + (o.size || 20) + "px " + (o.family || FONT);
|
| 163 |
+
ctx.textAlign = o.align || "left"; ctx.textBaseline = o.baseline || "alphabetic";
|
| 164 |
+
ctx.fillStyle = o.color || "#000"; ctx.fillText(str, x, y);
|
| 165 |
+
ctx.restore();
|
| 166 |
+
}
|
| 167 |
+
function arrow(ctx, x1, y1, x2, y2, p, color, w) {
|
| 168 |
+
if (p <= 0) return;
|
| 169 |
+
var x = T.lerp(x1, x2, p), y = T.lerp(y1, y2, p), a = Math.atan2(y2 - y1, x2 - x1), k = 4 + (w || 3) * 2.6;
|
| 170 |
+
ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = w || 3; ctx.lineCap = "round"; ctx.lineJoin = "round";
|
| 171 |
+
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x, y);
|
| 172 |
+
ctx.moveTo(x - k * Math.cos(a - 0.45), y - k * Math.sin(a - 0.45)); ctx.lineTo(x, y); ctx.lineTo(x - k * Math.cos(a + 0.45), y - k * Math.sin(a + 0.45));
|
| 173 |
+
ctx.stroke(); ctx.restore();
|
| 174 |
+
}
|
| 175 |
+
function check(ctx, x, y, ok, p, th) {
|
| 176 |
+
if (p <= 0) return;
|
| 177 |
+
var s = T.pop(p);
|
| 178 |
+
ctx.save(); ctx.translate(x, y); ctx.scale(s, s);
|
| 179 |
+
ctx.beginPath(); ctx.arc(0, 0, 13, 0, 2 * Math.PI); ctx.fillStyle = ok ? th.good : th.bad; ctx.fill();
|
| 180 |
+
ctx.strokeStyle = "#fff"; ctx.lineWidth = 3.2; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.beginPath();
|
| 181 |
+
if (ok) { ctx.moveTo(-5.5, 0); ctx.lineTo(-1.5, 4.5); ctx.lineTo(6, -5); } else { ctx.moveTo(-5, -5); ctx.lineTo(5, 5); ctx.moveTo(5, -5); ctx.lineTo(-5, 5); }
|
| 182 |
+
ctx.stroke(); ctx.restore();
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
window.Prism3D = {FONT: FONT, MONO: MONO, MATH: MATH, SITE: SITE, T: T, isDark: isDark, theme: theme, rgbOf: rgbOf, mix: mix, css: css,
|
| 186 |
+
camera: camera, sphere: sphere, glass: glass, line3: line3, perovskite: perovskite, crystal: crystal,
|
| 187 |
+
rrect: rrect, text: text, arrow: arrow, check: check};
|
| 188 |
+
})();
|