Babu09 commited on
Commit
ef5573e
·
verified ·
1 Parent(s): d62dabd

Deploy MEIDNet Studio

Browse files
meidnet/studio/landing.html CHANGED
@@ -20,12 +20,13 @@
20
  @font-face{font-family:'STIX Two Text';font-style:normal;font-weight:400 700;font-display:swap;src:url(/docs/assets/fonts/stixtwotext-normal-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
21
  @font-face{font-family:'STIX Two Text';font-style:italic;font-weight:400 700;font-display:swap;src:url(/docs/assets/fonts/stixtwotext-italic-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
22
  @font-face{font-family:'STIX Two Text';font-style:italic;font-weight:400 700;font-display:swap;src:url(/docs/assets/fonts/stixtwotext-italic-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
 
23
  @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/docs/assets/fonts/ibmplexmono-normal-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
24
  :root{--c-data:#2a78d6;--c-model:#eb6834;--c-family:#1baf7a;--c-rules:#eda100;--c-targets:#e87ba4;--c-search:#008300;--c-candidates:#4a3aa7;
25
  --p1:#4f46e5;--p2:#9333ea;--bg:#f6f7f5;--card:#fff;--ink:#0b0b0b;--muted:#52514e;--faint:#898781;--line:#e1e0d9;
26
  --link:#4f46e5;--eyebrow-mix:#000;
27
  --serif:'STIX Two Text','Cambria','Sitka Text',Georgia,serif;--math:'STIX Two Text','Cambria Math','STIX Two Math','Latin Modern Math',serif;
28
- --mono:'IBM Plex Mono',ui-monospace,'Cascadia Mono',Consolas,monospace}
29
  :root[data-theme="dark"]{--bg:#111417;--card:#1b1f24;--ink:#fff;--muted:#c3c2b7;--faint:#898781;--line:#2c2c2a;--link:#a5b4fc;--eyebrow-mix:#fff}
30
  @media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#111417;--card:#1b1f24;--ink:#fff;--muted:#c3c2b7;--faint:#898781;--line:#2c2c2a;--link:#a5b4fc;--eyebrow-mix:#fff}}
31
  *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
@@ -37,28 +38,39 @@ header .wrap{display:flex;align-items:center;gap:10px;min-height:56px;flex-wrap:
37
  nav{margin-left:auto;display:flex;gap:2px;flex-wrap:wrap}nav a{text-decoration:none;color:var(--muted);padding:6px 9px;border-radius:8px;font-size:14px;font-weight:600;white-space:nowrap}
38
  nav a:hover,nav a[aria-current]{background:var(--bg);color:var(--ink)}nav a.cta{background:var(--p1);color:#fff}nav a.cta:hover{background:var(--p2);color:#fff}
39
  @media(max-width:1080px){nav{order:3;width:100%;margin-left:0;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;padding:0 0 8px}nav::-webkit-scrollbar{display:none}header .theme{margin-left:auto}}
40
- .hero{display:grid;grid-template-columns:1.12fr 1fr;gap:40px;align-items:center;padding:52px 0 28px}
41
- @media(max-width:860px){.hero{grid-template-columns:1fr}}
 
 
42
  .hero>*{min-width:0}
43
- h1{font:600 clamp(38px,4.5vw,60px)/1.06 var(--serif);letter-spacing:-.012em;margin:0 0 20px;text-wrap:balance;font-kerning:normal}
44
- h1 .prism{font-style:italic;padding-right:.08em;background:linear-gradient(90deg,var(--p1),var(--p2) 60%,#db2777);-webkit-background-clip:text;background-clip:text;color:transparent}
45
- h1 .thin{font-weight:400}
46
- .tag{font-size:clamp(17px,1.55vw,19.5px);line-height:1.6;color:var(--muted);margin:0 0 24px;max-width:46ch}
 
 
 
 
 
47
  .btns{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 16px}
48
  .btn{display:inline-flex;align-items:center;gap:8px;border-radius:12px;padding:12px 18px;font-weight:700;text-decoration:none;border:1px solid var(--line);background:var(--card);color:var(--ink);font-size:16px}
49
  .btn.primary{background:var(--p1);border-color:var(--p1);color:#fff}.btn.primary:hover{background:var(--p2);border-color:var(--p2)}.btn:hover{box-shadow:0 4px 14px -8px rgba(0,0,0,.5)}
50
  .pub{font:15.5px/1.5 var(--serif);color:var(--muted);margin:0}.pub b{color:var(--ink)}
51
- .prismfig{width:100%;height:auto;display:block;border-radius:14px;background:#fff;border:1px solid var(--line)}
52
- .heroart{display:flex;flex-direction:column;gap:18px}
53
- .eqs{display:grid;grid-template-columns:5.2rem minmax(0,1fr);gap:9px 16px;align-items:baseline;margin:0;padding:0 4px}
54
- .eqs dt{font:500 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:color-mix(in oklab,var(--k) 78%,var(--eyebrow-mix))}
55
- .eqs dd{margin:0;font:italic 400 clamp(15px,1.45vw,18.5px)/1.3 var(--math);color:var(--muted);white-space:nowrap}
56
- .eqs dd .up{font-style:normal}.eqs sub,.eqs sup{font-size:.68em;line-height:0}
57
- .eqs dd{overflow-x:auto;scrollbar-width:none}.eqs dd::-webkit-scrollbar{display:none}
 
 
 
 
 
 
58
  .frac{display:inline-flex;flex-direction:column;align-items:center;vertical-align:middle;margin:0 .15em;line-height:1.15}
59
  .frac>span:first-child{border-bottom:1px solid currentColor;padding:0 .2em .05em}.frac>span:last-child{padding:.08em .2em 0}
60
- .eqs .note{font:400 12.5px/1.4 var(--serif);font-style:normal;color:var(--faint);white-space:normal}
61
- @media(max-width:480px){.eqs{gap:8px 10px;grid-template-columns:4.4rem minmax(0,1fr)}.eqs dd{font-size:14.5px}}
62
  .video{margin:12px 0 28px}.video video{width:100%;height:auto;border-radius:14px;border:1px solid var(--line);background:#000;display:block}
63
  .video figcaption{font-size:14px;color:var(--muted);margin-top:8px}
64
  .video video[hidden]{display:none}
@@ -109,16 +121,26 @@ footer{color:var(--faint);font-size:13.5px;padding:10px 0 40px;display:flex;gap:
109
  <main class="wrap">
110
  <section class="hero">
111
  <div>
112
- <h1>Learn, build and benchmark <span class="prism">multimodal AI</span> <span class="thin">for materials discovery</span></h1>
113
- <p class="tag">From the concepts of multimodal learning to a trained model and reproducible results. MEIDNet, the reference implementation, learns one space shared by crystal structures and their properties, and searches it for materials with target properties.</p>
 
114
  <div class="btns"><a class="btn primary" href="/studio/">▶ Try MEIDNet</a><a class="btn" href="/studio/?panel=data">Use your dataset</a><a class="btn" href="/docs/learn/index.html">Learn multimodality</a></div>
115
  <p class="pub">A. Babu <i>et al.</i>, <i>npj Comput. Mater.</i> <b>12</b>, 287 (2026) · <a href="https://doi.org/10.1038/s41524-026-02153-3">Article</a> · <a href="https://github.com/ABnano/MEIDNet">Code</a></p>
116
  </div>
117
- <div class="heroart">
118
- <dl class="eqs" aria-label="Representation learning" style="--k:var(--c-model)"><dt style="--k:var(--c-model)">Encode</dt><dd>m<sub>ij</sub> = φ<sub>e</sub>(h<sub>i</sub>, h<sub>j</sub>, ‖<b>x</b><sub>i</sub> − <b>x</b><sub>j</sub>‖²)</dd><dt></dt><dd>h<sub>i</sub> ← φ<sub>h</sub>(h<sub>i</sub>, Σ<sub>j</sub> m<sub>ij</sub>)</dd><dt style="--k:var(--c-family)">Align</dt><dd>s<sub>ij</sub> = cos(z<sub>s,i</sub>, z<sub>p,j</sub>)</dd><dt></dt><dd>ℒ = −log <span class="frac"><span>exp(s<sub>ii</sub>/τ)</span><span>Σ<sub>j</sub> exp(s<sub>ij</sub>/τ)</span></span></dd><dt style="--k:var(--p2)">Fuse</dt><dd>z = ½ (z<sub>s</sub> + z<sub>p</sub>) <span class="up">∈ ℝ<sup>128</sup></span></dd></dl>
119
- <img class="prismfig" src="/docs/assets/meidnet_prism_logo.png" alt="MEIDNet Prism: several modalities enter a prism and leave it as one connected representation of materials.">
120
- <dl class="eqs" aria-label="Inverse design"><dt style="--k:var(--c-targets)">Design</dt><dd>z<sup>*</sup> = argmin<sub>z</sub> ‖g(z) − y<sup>*</sup>‖²</dd><dt style="--k:var(--c-candidates)">Decode</dt><dd>x<sup>*</sup> = D(z<sup>*</sup>)</dd><dt style="--k:var(--c-search)">Validate</dt><dd>E<sub>hull</sub>(x<sup>*</sup>) ≤ ε</dd></dl>
121
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </section>
123
 
124
  <figure class="video" id="tour">
@@ -201,6 +223,7 @@ footer{color:var(--faint);font-size:13.5px;padding:10px 0 40px;display:flex;gap:
201
  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'; } };
202
  })();
203
  </script>
 
204
  <script src="/ask-prism.js" defer></script>
205
  </body>
206
  </html>
 
20
  @font-face{font-family:'STIX Two Text';font-style:normal;font-weight:400 700;font-display:swap;src:url(/docs/assets/fonts/stixtwotext-normal-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
21
  @font-face{font-family:'STIX Two Text';font-style:italic;font-weight:400 700;font-display:swap;src:url(/docs/assets/fonts/stixtwotext-italic-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
22
  @font-face{font-family:'STIX Two Text';font-style:italic;font-weight:400 700;font-display:swap;src:url(/docs/assets/fonts/stixtwotext-italic-greek.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF}
23
+ @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:500 800;font-display:swap;src:url(/docs/assets/fonts/plusjakartasans-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
24
  @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/docs/assets/fonts/ibmplexmono-normal-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
25
  :root{--c-data:#2a78d6;--c-model:#eb6834;--c-family:#1baf7a;--c-rules:#eda100;--c-targets:#e87ba4;--c-search:#008300;--c-candidates:#4a3aa7;
26
  --p1:#4f46e5;--p2:#9333ea;--bg:#f6f7f5;--card:#fff;--ink:#0b0b0b;--muted:#52514e;--faint:#898781;--line:#e1e0d9;
27
  --link:#4f46e5;--eyebrow-mix:#000;
28
  --serif:'STIX Two Text','Cambria','Sitka Text',Georgia,serif;--math:'STIX Two Text','Cambria Math','STIX Two Math','Latin Modern Math',serif;
29
+ --display:'Plus Jakarta Sans',system-ui,'Segoe UI',sans-serif;--mono:'IBM Plex Mono',ui-monospace,'Cascadia Mono',Consolas,monospace}
30
  :root[data-theme="dark"]{--bg:#111417;--card:#1b1f24;--ink:#fff;--muted:#c3c2b7;--faint:#898781;--line:#2c2c2a;--link:#a5b4fc;--eyebrow-mix:#fff}
31
  @media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#111417;--card:#1b1f24;--ink:#fff;--muted:#c3c2b7;--faint:#898781;--line:#2c2c2a;--link:#a5b4fc;--eyebrow-mix:#fff}}
32
  *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
 
38
  nav{margin-left:auto;display:flex;gap:2px;flex-wrap:wrap}nav a{text-decoration:none;color:var(--muted);padding:6px 9px;border-radius:8px;font-size:14px;font-weight:600;white-space:nowrap}
39
  nav a:hover,nav a[aria-current]{background:var(--bg);color:var(--ink)}nav a.cta{background:var(--p1);color:#fff}nav a.cta:hover{background:var(--p2);color:#fff}
40
  @media(max-width:1080px){nav{order:3;width:100%;margin-left:0;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;padding:0 0 8px}nav::-webkit-scrollbar{display:none}header .theme{margin-left:auto}}
41
+ .hero{position:relative;min-height:540px;padding:48px 0 22px;display:flex;align-items:center}
42
+ .hero .copy{position:relative;z-index:2;max-width:640px}
43
+ .hero::before{content:"";position:absolute;z-index:-1;inset:-30px 0 -10px;pointer-events:none;background:radial-gradient(42% 58% at 78% 42%,rgba(99,102,241,.13),transparent 70%),radial-gradient(34% 44% at 62% 82%,rgba(20,184,166,.11),transparent 70%),radial-gradient(30% 40% at 12% 18%,rgba(59,130,246,.07),transparent 70%)}
44
+ @media(max-width:900px){.hero{display:block;min-height:0;padding-top:28px}}
45
  .hero>*{min-width:0}
46
+ h1.mh{font:800 clamp(25px,3.05vw,40px)/1.14 var(--display);letter-spacing:-.022em;margin:0 0 22px;color:var(--ink)}
47
+ h1.mh .ln{display:block}h1.mh .for{font-weight:700;letter-spacing:-.01em}
48
+ .mol{background:linear-gradient(90deg,#2f6fe4,#8b5cf6);-webkit-background-clip:text;background-clip:text;color:transparent}
49
+ .mol.mol-on{background:none;color:inherit}.mol.mol-on>span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
50
+ .mol svg{display:inline-block;height:auto;max-width:100%;overflow:visible}
51
+ .mol-shadow{fill:none;stroke:rgba(30,41,90,.13);stroke-width:.15;stroke-linecap:round;stroke-linejoin:round}
52
+ :root[data-theme="dark"] .mol-shadow{stroke:rgba(0,0,0,.45)}
53
+ @media (prefers-color-scheme: dark){:root:not([data-theme="light"]) .mol-shadow{stroke:rgba(0,0,0,.45)}}
54
+ .tag{font-size:clamp(16.5px,1.45vw,19px);line-height:1.62;color:var(--muted);margin:0 0 24px;max-width:50ch}
55
  .btns{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 16px}
56
  .btn{display:inline-flex;align-items:center;gap:8px;border-radius:12px;padding:12px 18px;font-weight:700;text-decoration:none;border:1px solid var(--line);background:var(--card);color:var(--ink);font-size:16px}
57
  .btn.primary{background:var(--p1);border-color:var(--p1);color:#fff}.btn.primary:hover{background:var(--p2);border-color:var(--p2)}.btn:hover{box-shadow:0 4px 14px -8px rgba(0,0,0,.5)}
58
  .pub{font:15.5px/1.5 var(--serif);color:var(--muted);margin:0}.pub b{color:var(--ink)}
59
+ .scene{position:absolute;z-index:1;right:-36px;top:0;bottom:0;width:56%;margin:0}
60
+ .scene canvas{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:grab;touch-action:pan-y}
61
+ .scene canvas.grabbing{cursor:grabbing}
62
+ .scene figcaption{position:absolute;right:4px;bottom:0;font:500 11.5px/1.3 var(--mono);color:var(--faint);letter-spacing:.02em}
63
+ @media(max-width:900px){.scene{position:relative;right:auto;width:100%;max-width:560px;aspect-ratio:1/.86;margin:-28px auto 0}}
64
+ .method{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.2fr) minmax(0,1.05fr) minmax(0,1.3fr) minmax(0,.8fr) minmax(0,.9fr);gap:0;margin:6px 0 30px;border:1px solid var(--line);border-radius:14px;background:var(--card);overflow:hidden}
65
+ .method div{padding:12px 12px 13px;border-left:1px solid var(--line);position:relative}.method div:first-child{border-left:0}
66
+ .method div:not(:first-child)::before{content:"→";position:absolute;left:-7px;top:11px;font-size:12px;color:var(--faint);background:var(--card);line-height:1}
67
+ .method b{display:block;font:500 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;margin-bottom:9px;color:color-mix(in oklab,var(--k) 78%,var(--eyebrow-mix))}
68
+ .method .eq{display:block;font:italic 400 clamp(14.5px,1.15vw,16.5px)/1.35 var(--math);color:var(--ink);white-space:nowrap;overflow-x:auto;scrollbar-width:none}
69
+ .method .eq::-webkit-scrollbar{display:none}.method .up{font-style:normal}.method sub,.method sup{font-size:.68em;line-height:0}
70
+ @media(max-width:1000px){.method{grid-template-columns:repeat(3,minmax(0,1fr))}.method div:nth-child(4){border-left:0}.method div:nth-child(n+4){border-top:1px solid var(--line)}.method div:nth-child(4)::before{display:none}}
71
+ @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}}
72
  .frac{display:inline-flex;flex-direction:column;align-items:center;vertical-align:middle;margin:0 .15em;line-height:1.15}
73
  .frac>span:first-child{border-bottom:1px solid currentColor;padding:0 .2em .05em}.frac>span:last-child{padding:.08em .2em 0}
 
 
74
  .video{margin:12px 0 28px}.video video{width:100%;height:auto;border-radius:14px;border:1px solid var(--line);background:#000;display:block}
75
  .video figcaption{font-size:14px;color:var(--muted);margin-top:8px}
76
  .video video[hidden]{display:none}
 
121
  <main class="wrap">
122
  <section class="hero">
123
  <div>
124
+ <div class="copy">
125
+ <h1 class="mh"><span class="ln">Learn, build and benchmark </span><span class="ln"><span class="mol" data-mol="a"><span>multimodal AI</span></span> <span class="for">for</span> </span><span class="ln"><span class="mol" data-mol="b"><span>materials discovery</span></span></span></h1>
126
+ <p class="tag">Go from multimodal learning to working models and reproducible materials-discovery workflows. Explore architectures, train on your own data, compare results, and use the MEIDNet methodology to learn shared representations of materials and their properties.</p>
127
  <div class="btns"><a class="btn primary" href="/studio/">▶ Try MEIDNet</a><a class="btn" href="/studio/?panel=data">Use your dataset</a><a class="btn" href="/docs/learn/index.html">Learn multimodality</a></div>
128
  <p class="pub">A. Babu <i>et al.</i>, <i>npj Comput. Mater.</i> <b>12</b>, 287 (2026) · <a href="https://doi.org/10.1038/s41524-026-02153-3">Article</a> · <a href="https://github.com/ABnano/MEIDNet">Code</a></p>
129
  </div>
 
 
 
 
130
  </div>
131
+ <figure class="scene">
132
+ <canvas id="hero3d" role="img" aria-label="A cubic ABX3 perovskite: eight corner-sharing BX6 octahedra around an A-site cation, with panels for its crystal graph, a band-gap property curve and the shared latent space where structures and properties are paired."></canvas>
133
+ <figcaption>cubic ABX<sub>3</sub> perovskite · drag to rotate</figcaption>
134
+ </figure>
135
+ </section>
136
+
137
+ <section class="method" aria-label="The method in six steps">
138
+ <div style="--k:var(--c-model)"><b>Encode</b><span class="eq">h<sub>i</sub> ← φ<sub>h</sub>(h<sub>i</sub>, Σ<sub>j</sub> m<sub>ij</sub>)</span></div>
139
+ <div style="--k:var(--c-family)"><b>Align</b><span class="eq">ℒ = −log <span class="frac"><span>exp(s<sub>ii</sub>/τ)</span><span>Σ<sub>j</sub> exp(s<sub>ij</sub>/τ)</span></span></span></div>
140
+ <div style="--k:var(--p2)"><b>Fuse</b><span class="eq">z = ½ (z<sub>s</sub> + z<sub>p</sub>) <span class="up">∈ ℝ<sup>128</sup></span></span></div>
141
+ <div style="--k:var(--c-targets)"><b>Design</b><span class="eq">z<sup>*</sup> = argmin<sub>z</sub> ‖g(z) − y<sup>*</sup>‖²</span></div>
142
+ <div style="--k:var(--c-candidates)"><b>Decode</b><span class="eq">x<sup>*</sup> = D(z<sup>*</sup>)</span></div>
143
+ <div style="--k:var(--c-search)"><b>Validate</b><span class="eq">E<sub>hull</sub>(x<sup>*</sup>) ≤ ε</span></div>
144
  </section>
145
 
146
  <figure class="video" id="tour">
 
223
  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'; } };
224
  })();
225
  </script>
226
+ <script src="/docs/assets/prism-hero.js" defer></script>
227
  <script src="/ask-prism.js" defer></script>
228
  </body>
229
  </html>
site/assets/fonts/OFL-PlusJakartaSans.txt ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2020 The Plus Jakarta Sans Project Authors (https://github.com/tokotype/PlusJakartaSans)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
site/assets/fonts/plusjakartasans-latin.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:153fc85b70298beeb1d61a5f723331649e7f23bb77302a66e61cb3e2fbdb5e79
3
+ size 27348
site/assets/prism-hero.js ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* MEIDNet Prism landing page: the molecular lettering of the headline and the 3D crystal scene of the hero.
2
+ No libraries: the lettering is SVG built from stroke glyphs, the scene a small canvas renderer (painter's algorithm).
3
+ The headline stays real text for search engines and screen readers; without JavaScript it shows as plain text. */
4
+ (function () {
5
+ "use strict";
6
+ var NS = "http://www.w3.org/2000/svg";
7
+
8
+ function isDark() {
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
+ /* ═══════════ molecular lettering: bonds along the strokes, atoms at the joints ═══════════ */
15
+ var ASC = 1.42, DESC = 0.48, PAD = 0.24, GAP = 0.36, U = 0.76; // x-height = 1 unit; U = unit in em
16
+
17
+ function arc(cx, cy, rx, ry, a0, a1, every) {
18
+ var n = Math.max(8, Math.ceil(Math.abs(a1 - a0) / 6)), pts = [], beads = [];
19
+ for (var i = 0; i <= n; i++) {
20
+ var a = (a0 + (a1 - a0) * i / n) * Math.PI / 180;
21
+ pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
22
+ }
23
+ var k = Math.max(1, Math.round(n * every / Math.abs(a1 - a0)));
24
+ for (var j = 0; j <= n; j += k) beads.push(pts[j]);
25
+ beads.push(pts[n]);
26
+ return {pts: pts, beads: beads};
27
+ }
28
+ function line() { var p = Array.prototype.slice.call(arguments); return {pts: p, beads: p}; }
29
+ function dot(x, y) { return {pts: [], beads: [[x, y]]}; }
30
+ var ring = function () { return arc(0.5, 0.5, 0.5, 0.5, 0, 360, 72); };
31
+
32
+ var G = {
33
+ a: {w: 1.0, s: function () { return [ring(), line([1, 1], [1, 0])]; }},
34
+ c: {w: 0.92, s: function () { return [arc(0.5, 0.5, 0.5, 0.5, 40, 320, 70)]; }},
35
+ d: {w: 1.0, s: function () { return [ring(), line([1, ASC], [1, 0])]; }},
36
+ e: {w: 1.0, s: function () { return [line([0.02, 0.5], [1, 0.5]), arc(0.5, 0.5, 0.5, 0.5, 0, 318, 80)]; }},
37
+ i: {w: 0.2, s: function () { return [line([0.1, 0], [0.1, 1]), dot(0.1, 1.34)]; }},
38
+ l: {w: 0.2, s: function () { return [line([0.1, 0], [0.1, 0.71], [0.1, ASC])]; }},
39
+ m: {w: 1.3, s: function () {
40
+ return [line([0, 0], [0, 1]), arc(0.325, 0.62, 0.325, 0.38, 180, 0, 90), line([0.65, 0.62], [0.65, 0]),
41
+ arc(0.975, 0.62, 0.325, 0.38, 180, 0, 90), line([1.3, 0.62], [1.3, 0])]; }},
42
+ o: {w: 1.0, s: function () { return [arc(0.5, 0.5, 0.5, 0.5, 90, 450, 60)]; }},
43
+ r: {w: 0.68, s: function () { return [line([0, 0], [0, 1]), arc(0.42, 0.55, 0.42, 0.42, 180, 60, 60)]; }},
44
+ s: {w: 0.82, s: function () { return [arc(0.41, 0.75, 0.36, 0.25, 20, 270, 85), arc(0.41, 0.25, 0.39, 0.25, 90, -160, 85)]; }},
45
+ t: {w: 0.72, s: function () { return [line([0.28, 1.32], [0.28, 1], [0.28, 0]), line([0, 1], [0.66, 1])]; }},
46
+ u: {w: 0.92, s: function () { return [line([0, 1], [0, 0.46]), arc(0.46, 0.46, 0.46, 0.46, 180, 360, 90), line([0.92, 1], [0.92, 0])]; }},
47
+ v: {w: 0.92, s: function () { return [line([0, 1], [0.46, 0], [0.92, 1])]; }},
48
+ y: {w: 0.92, s: function () { return [line([0, 1], [0.46, 0]), line([0.92, 1], [0.46, 0], [0.24, -0.48])]; }},
49
+ A: {w: 1.16, s: function () { return [line([0, 0], [0.58, ASC], [1.16, 0]), line([0.25, 0.55], [0.91, 0.55])]; }},
50
+ I: {w: 0.2, s: function () { return [line([0.1, 0], [0.1, 0.71], [0.1, ASC])]; }},
51
+ " ": {w: 0.38, s: function () { return []; }}
52
+ };
53
+
54
+ var lettering = 0;
55
+ function moleculeSvg(text, stops) {
56
+ var id = "mol" + (++lettering), x = PAD, paths = [], beads = [];
57
+ for (var c = 0; c < text.length; c++) {
58
+ var g = G[text[c]];
59
+ if (!g) return null; // a glyph we do not draw: keep the plain text
60
+ g.s().forEach(function (st) {
61
+ if (st.pts.length) paths.push(st.pts.map(function (p) { return [p[0] + x, p[1]]; }));
62
+ st.beads.forEach(function (b) {
63
+ var q = [b[0] + x, b[1]];
64
+ if (!beads.some(function (o) { return Math.hypot(o[0] - q[0], o[1] - q[1]) < 0.06; })) beads.push(q);
65
+ });
66
+ });
67
+ x += g.w + GAP;
68
+ }
69
+ var W = x - GAP + PAD, H = ASC + DESC + 2 * PAD;
70
+ var Y = function (y) { return (ASC + PAD - y).toFixed(3); };
71
+ var d = paths.map(function (p) { return "M" + p.map(function (q) { return q[0].toFixed(3) + " " + Y(q[1]); }).join("L"); }).join("");
72
+ var svg = document.createElementNS(NS, "svg");
73
+ svg.setAttribute("viewBox", "0 0 " + W.toFixed(3) + " " + H.toFixed(3));
74
+ svg.setAttribute("aria-hidden", "true");
75
+ svg.setAttribute("focusable", "false");
76
+ svg.style.width = (W * U).toFixed(3) + "em";
77
+ svg.style.verticalAlign = (-(DESC + PAD) * U).toFixed(3) + "em";
78
+ svg.innerHTML =
79
+ '<defs><linearGradient id="' + id + 'g" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="' + W.toFixed(2) + '" y2="0">' +
80
+ stops.map(function (s) { return '<stop offset="' + s[0] + '" stop-color="' + s[1] + '"/>'; }).join("") + "</linearGradient>" +
81
+ '<radialGradient id="' + id + 's" cx=".36" cy=".32" r=".68"><stop offset="0" stop-color="#fff" stop-opacity=".95"/>' +
82
+ '<stop offset=".38" stop-color="#fff" stop-opacity=".28"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient></defs>' +
83
+ '<path class="mol-shadow" d="' + d + '" transform="translate(.05 .07)"/>' +
84
+ '<path d="' + d + '" fill="none" stroke="url(#' + id + 'g)" stroke-width=".2" stroke-linecap="round" stroke-linejoin="round"/>' +
85
+ '<path d="' + d + '" fill="none" stroke="#fff" stroke-opacity=".45" stroke-width=".05" stroke-linecap="round" stroke-linejoin="round" transform="translate(-.03 -.04)"/>' +
86
+ beads.map(function (b) {
87
+ var cx = b[0].toFixed(3), cy = Y(b[1]);
88
+ return '<circle cx="' + cx + '" cy="' + cy + '" r=".158" fill="url(#' + id + 'g)"/>' +
89
+ '<circle cx="' + cx + '" cy="' + cy + '" r=".158" fill="url(#' + id + 's)"/>';
90
+ }).join("");
91
+ return svg;
92
+ }
93
+
94
+ var PALETTES = {
95
+ a: [[0, "#2f6fe4"], [0.45, "#6366f1"], [0.8, "#8b5cf6"], [1, "#a855f7"]],
96
+ b: [[0, "#2563eb"], [0.3, "#0e7490"], [0.55, "#0d9488"], [1, "#10b981"]]
97
+ };
98
+ document.querySelectorAll("[data-mol]").forEach(function (span) {
99
+ var svg = moleculeSvg(span.textContent.trim(), PALETTES[span.getAttribute("data-mol")] || PALETTES.a);
100
+ if (!svg) return;
101
+ span.classList.add("mol-on");
102
+ span.insertBefore(svg, span.firstChild);
103
+ });
104
+
105
+ /* ═══════════ the 3D scene: an ABX3 perovskite and what MEIDNet makes of it ═══════════ */
106
+ var canvas = document.getElementById("hero3d");
107
+ if (!canvas || !canvas.getContext) return;
108
+ var ctx = canvas.getContext("2d");
109
+ var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
110
+
111
+ // crystal: the cubic ABX3 cell, A cations on the corners, the BX6 octahedron in the centre (X on the face centres)
112
+ var a = 1.25, B = [[0, 0, 0]], A = [], EDGES = [], FACES = [];
113
+ var X = [[a, 0, 0], [-a, 0, 0], [0, a, 0], [0, -a, 0], [0, 0, a], [0, 0, -a]];
114
+ [-a, a].forEach(function (x) { [-a, a].forEach(function (y) { [-a, a].forEach(function (z) { A.push([x, y, z]); }); }); });
115
+ A.forEach(function (p) { A.forEach(function (q) { // the cube: corners one lattice step apart
116
+ var d = Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
117
+ if (Math.abs(d - 2 * a) < 1e-6 && p < q) EDGES.push([p, q]);
118
+ }); });
119
+ [[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) {
120
+ FACES.push([X[f[0]], X[f[1]], X[f[2]]]);
121
+ });
122
+
123
+ // panels behind the crystal: centre, half sizes, rotation about y, what they show
124
+ function rng(seed) { return function () { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; }; }
125
+ var r = rng(7), LATENT = [];
126
+ for (var i = 0; i < 90; i++) { // paired points: a structure (teal) and its properties (violet), close together
127
+ 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;
128
+ LATENT.push({u: cx, v: cy, du: (r() - 0.5) * 0.09, dv: (r() - 0.5) * 0.09, c: t});
129
+ }
130
+ var GRAPH = {n: [[-0.62, -0.35], [-0.22, 0.42], [0.18, -0.12], [0.6, 0.38], [0.52, -0.5], [-0.05, -0.62], [-0.68, 0.38]],
131
+ e: [[0, 1], [1, 2], [2, 3], [2, 4], [0, 5], [5, 2], [1, 6], [6, 0], [3, 4]]};
132
+ var PANELS = [
133
+ {c: [-2.2, 1.5, -2.4], h: [0.95, 0.68], ry: 0.5, kind: "graph", label: "crystal graph"},
134
+ {c: [1.45, 2.0, -2.9], h: [1.15, 0.75], ry: -0.32, kind: "latent", label: "latent space z ∈ ℝ¹²⁸"},
135
+ {c: [2.25, -1.25, -1.6], h: [0.8, 0.56], ry: -0.62, kind: "property", label: "property: band gap"}
136
+ ];
137
+
138
+ var W = 0, H = 0, DPR = 1, yaw = 0, pitch = 0, dragYaw = 0, dragPitch = 0, t0 = performance.now();
139
+ function resize() {
140
+ var b = canvas.getBoundingClientRect();
141
+ DPR = Math.min(2, window.devicePixelRatio || 1);
142
+ W = Math.max(1, b.width); H = Math.max(1, b.height);
143
+ canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
144
+ draw();
145
+ }
146
+ function proj(p) {
147
+ var cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch);
148
+ var x1 = p[0] * cy + p[2] * sy, z1 = -p[0] * sy + p[2] * cy, y1 = p[1];
149
+ var y2 = y1 * cp - z1 * sp, z2 = y1 * sp + z1 * cp;
150
+ var D = 9, F = Math.min(W, H * 1.1) * D / 8.3, k = F / (D - z2);
151
+ return [W * 0.52 + x1 * k, H * 0.5 - y2 * k, z2, k];
152
+ }
153
+ function panelPoint(P, u, v, w) { // panel-local (u, v in [-1, 1]) to world
154
+ var c = Math.cos(P.ry), sn = Math.sin(P.ry), x = u * P.h[0], y = v * P.h[1];
155
+ return [P.c[0] + x * c + (w || 0) * sn, P.c[1] + y, P.c[2] - x * sn + (w || 0) * c];
156
+ }
157
+ 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(); }
158
+
159
+ function sphere(p, rad, col, dark) {
160
+ var q = proj(p), R = rad * q[3];
161
+ var g = ctx.createRadialGradient(q[0] - R * 0.38, q[1] - R * 0.42, R * 0.08, q[0], q[1], R);
162
+ g.addColorStop(0, "#ffffff"); g.addColorStop(0.22, col[0]); g.addColorStop(0.72, col[1]); g.addColorStop(1, col[2]);
163
+ ctx.beginPath(); ctx.arc(q[0], q[1], R, 0, 2 * Math.PI);
164
+ ctx.fillStyle = g; ctx.fill();
165
+ ctx.lineWidth = 0.8; ctx.strokeStyle = dark ? "rgba(0,0,0,.35)" : "rgba(15,23,42,.12)"; ctx.stroke();
166
+ }
167
+
168
+ function drawPanel(P, dark) {
169
+ var corners = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(function (c) { return proj(panelPoint(P, c[0], c[1])); });
170
+ poly(corners);
171
+ var g = ctx.createLinearGradient(corners[3][0], corners[3][1], corners[1][0], corners[1][1]);
172
+ g.addColorStop(0, dark ? "rgba(51,65,85,.55)" : "rgba(255,255,255,.78)");
173
+ g.addColorStop(1, dark ? "rgba(30,41,59,.28)" : "rgba(226,232,255,.42)");
174
+ ctx.fillStyle = g; ctx.fill();
175
+ ctx.lineWidth = 1; ctx.strokeStyle = dark ? "rgba(148,163,184,.45)" : "rgba(148,163,184,.55)"; ctx.stroke();
176
+ var L = function (u, v) { return proj(panelPoint(P, u, v)); };
177
+ if (P.kind === "graph") {
178
+ ctx.lineWidth = 1.4; ctx.strokeStyle = dark ? "rgba(148,163,184,.75)" : "rgba(100,116,139,.6)";
179
+ GRAPH.e.forEach(function (e) { var a = L(GRAPH.n[e[0]][0] * 0.8, GRAPH.n[e[0]][1] * 0.8), b = L(GRAPH.n[e[1]][0] * 0.8, GRAPH.n[e[1]][1] * 0.8);
180
+ ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); });
181
+ GRAPH.n.forEach(function (n, j) { var q = L(n[0] * 0.8, n[1] * 0.8);
182
+ ctx.beginPath(); ctx.arc(q[0], q[1], Math.max(2.5, 0.07 * q[3]), 0, 2 * Math.PI);
183
+ ctx.fillStyle = ["#3b82f6", "#14b8a6", "#8b5cf6"][j % 3]; ctx.fill(); });
184
+ } else if (P.kind === "latent") {
185
+ LATENT.forEach(function (p) {
186
+ var a = L(p.u, p.v), b = L(p.u + p.du, p.v + p.dv), rad = Math.max(1.4, 0.028 * a[3]);
187
+ ctx.strokeStyle = dark ? "rgba(203,213,225,.3)" : "rgba(100,116,139,.28)"; ctx.lineWidth = 0.8;
188
+ ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
189
+ var h = 175 + (p.c + 1) * 50;
190
+ ctx.fillStyle = "hsla(" + h + ",70%," + (dark ? 62 : 48) + "%,.9)";
191
+ ctx.beginPath(); ctx.arc(a[0], a[1], rad, 0, 2 * Math.PI); ctx.fill();
192
+ ctx.fillStyle = "hsla(" + (h + 80) + ",70%," + (dark ? 70 : 58) + "%,.85)";
193
+ ctx.beginPath(); ctx.arc(b[0], b[1], rad * 0.85, 0, 2 * Math.PI); ctx.fill();
194
+ });
195
+ } else {
196
+ ctx.strokeStyle = dark ? "rgba(148,163,184,.6)" : "rgba(100,116,139,.55)"; ctx.lineWidth = 1;
197
+ var o = L(-0.85, -0.7), xe = L(0.85, -0.7), ye = L(-0.85, 0.75);
198
+ ctx.beginPath(); ctx.moveTo(ye[0], ye[1]); ctx.lineTo(o[0], o[1]); ctx.lineTo(xe[0], xe[1]); ctx.stroke();
199
+ ctx.beginPath();
200
+ for (var j = 0; j <= 60; j++) {
201
+ var u = -0.85 + 1.7 * j / 60, xg = (u + 0.85) * 3, // band gap axis, 0 to ~5 eV
202
+ yv = Math.exp(-Math.pow((xg - 1.6) / 0.6, 2)) + 0.55 * Math.exp(-Math.pow((xg - 3.4) / 0.8, 2)),
203
+ q = L(u, -0.7 + 1.3 * yv);
204
+ j ? ctx.lineTo(q[0], q[1]) : ctx.moveTo(q[0], q[1]);
205
+ }
206
+ ctx.strokeStyle = "#8b5cf6"; ctx.lineWidth = 2; ctx.stroke();
207
+ var tu = -0.85 + 2.0 / 3, a = L(tu, -0.7), b = L(tu, 0.7); // the target, 2 eV
208
+ ctx.setLineDash([4, 3]); ctx.strokeStyle = "#e87ba4"; ctx.lineWidth = 1.4;
209
+ ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); ctx.setLineDash([]);
210
+ }
211
+ var tl = corners[3];
212
+ ctx.font = "500 " + Math.round(Math.max(10, Math.min(12.5, W / 46))) + "px 'IBM Plex Mono', ui-monospace, monospace";
213
+ ctx.fillStyle = dark ? "rgba(203,213,225,.85)" : "rgba(71,85,105,.9)";
214
+ ctx.fillText(P.label, Math.max(4, Math.min(tl[0] + 2, W - ctx.measureText(P.label).width - 4)), tl[1] - 7); // inside the canvas
215
+ }
216
+
217
+ function draw() {
218
+ if (!W) return;
219
+ var dark = isDark();
220
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
221
+ ctx.clearRect(0, 0, W, H);
222
+ // floor grid
223
+ ctx.lineWidth = 1; ctx.strokeStyle = dark ? "rgba(148,163,184,.12)" : "rgba(100,116,139,.14)";
224
+ for (var g = -3; g <= 3; g += 0.75) {
225
+ var a = proj([g, -2.05, -3]), b = proj([g, -2.05, 3]), c = proj([-3, -2.05, g]), d = proj([3, -2.05, g]);
226
+ ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.moveTo(c[0], c[1]); ctx.lineTo(d[0], d[1]); ctx.stroke();
227
+ }
228
+ // the cation's shadow on the floor
229
+ var f = proj([0, -2.05, 0]), sh = ctx.createRadialGradient(f[0], f[1], 0, f[0], f[1], 2.0 * f[3]);
230
+ sh.addColorStop(0, dark ? "rgba(0,0,0,.35)" : "rgba(30,41,90,.07)"); sh.addColorStop(1, "rgba(0,0,0,0)");
231
+ 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();
232
+ // panels, far first
233
+ PANELS.slice().sort(function (p, q) { return proj(p.c)[2] - proj(q.c)[2]; }).forEach(function (P) { drawPanel(P, dark); });
234
+ // crystal: faces and atoms sorted by depth
235
+ var items = [];
236
+ FACES.forEach(function (fc) {
237
+ var q = fc.map(proj), z = (q[0][2] + q[1][2] + q[2][2]) / 3;
238
+ items.push({z: z, f: fc, q: q});
239
+ });
240
+ B.forEach(function (p) { items.push({z: proj(p)[2], p: p, rad: 0.27, col: ["#93c5fd", "#3b82f6", "#1e3a8a"]}); });
241
+ X.forEach(function (p) { items.push({z: proj(p)[2], p: p, rad: 0.2, col: ["#99f6e4", "#14b8a6", "#115e59"]}); });
242
+ A.forEach(function (p) { items.push({z: proj(p)[2], p: p, rad: 0.25, col: ["#c4b5fd", "#8b5cf6", "#3b0764"]}); });
243
+ 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}); });
244
+ items.sort(function (m, n) { return m.z - n.z; });
245
+ var Ldir = [-0.45, 0.7, 0.55];
246
+ items.forEach(function (it) {
247
+ if (it.p) return sphere(it.p, it.rad, it.col, dark);
248
+ if (it.line) {
249
+ ctx.lineWidth = 1.2; ctx.strokeStyle = dark ? "rgba(148,163,184,.45)" : "rgba(100,116,139,.45)";
250
+ 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;
251
+ }
252
+ 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]];
253
+ 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]);
254
+ 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;
255
+ var lit = Math.abs(nx * Ldir[0] + ny * Ldir[1] + nz * Ldir[2]);
256
+ poly(it.q);
257
+ var gr = ctx.createLinearGradient(it.q[0][0], it.q[0][1], it.q[2][0], it.q[2][1]);
258
+ 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) + ")");
259
+ 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) + ")");
260
+ ctx.fillStyle = gr; ctx.fill();
261
+ ctx.lineWidth = 1; ctx.strokeStyle = dark ? "rgba(191,219,254,.5)" : "rgba(255,255,255,.95)"; ctx.stroke();
262
+ ctx.lineWidth = 0.6; ctx.strokeStyle = dark ? "rgba(96,165,250,.35)" : "rgba(99,102,241,.25)"; ctx.stroke();
263
+ });
264
+ }
265
+
266
+ var running = false, visible = true;
267
+ function frame(now) {
268
+ var t = (now - t0) / 1000;
269
+ yaw = -0.5 + 0.38 * Math.sin(t * 0.21) + dragYaw;
270
+ pitch = -0.33 + 0.05 * Math.sin(t * 0.16) + dragPitch;
271
+ draw();
272
+ if (running) requestAnimationFrame(frame);
273
+ }
274
+ function start() { if (reduce || running || !visible || document.hidden) return; running = true; requestAnimationFrame(frame); }
275
+ function stop() { running = false; }
276
+ if (reduce) { yaw = -0.5; pitch = -0.33; }
277
+
278
+ var drag = null;
279
+ canvas.addEventListener("pointerdown", function (e) { drag = {x: e.clientX, y: e.clientY, yaw: dragYaw, pitch: dragPitch}; canvas.setPointerCapture(e.pointerId); canvas.classList.add("grabbing"); });
280
+ canvas.addEventListener("pointermove", function (e) {
281
+ if (!drag) return;
282
+ dragYaw = drag.yaw + (e.clientX - drag.x) * 0.008;
283
+ dragPitch = Math.max(-0.5, Math.min(0.45, drag.pitch + (e.clientY - drag.y) * 0.005));
284
+ if (reduce) { yaw = -0.5 + dragYaw; pitch = -0.33 + dragPitch; draw(); }
285
+ });
286
+ function endDrag() { drag = null; canvas.classList.remove("grabbing"); }
287
+ canvas.addEventListener("pointerup", endDrag); canvas.addEventListener("pointercancel", endDrag);
288
+
289
+ if (window.ResizeObserver) new ResizeObserver(resize).observe(canvas); else window.addEventListener("resize", resize);
290
+ if (window.IntersectionObserver) new IntersectionObserver(function (es) { visible = es[0].isIntersecting; visible ? start() : stop(); }).observe(canvas);
291
+ document.addEventListener("visibilitychange", function () { document.hidden ? stop() : start(); });
292
+ var th = document.getElementById("theme");
293
+ if (th) th.addEventListener("click", function () { setTimeout(draw, 0); });
294
+ resize();
295
+ start();
296
+ })();