Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
824 lines (765 loc) · 53.1 KB
/
Copy pathindex.html
File metadata and controls
824 lines (765 loc) · 53.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
---
layout: raw
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zhanghao Zhouyin · Keep a light on for curiosity</title>
<meta name="description" content="Zhanghao Zhouyin — Physics PhD at McGill University, working across physics, AI, machine learning, and scientific computing. Building better scientific tools for discovery. 为好奇心留一盏灯。">
<meta name="keywords" content="Zhanghao Zhouyin, McGill University, Physics, AI for Science, Scientific computing, Machine learning, Materials simulation, Quantum transport, DeePTB">
<meta property="og:title" content="Keep a light on for curiosity — Zhanghao Zhouyin">
<meta property="og:description" content="A physicist working across physics, AI, and scientific software, building better tools for people who want to understand the world.">
<meta property="og:type" content="website">
<link rel="icon" type="image/svg+xml" href="{{ '/images/favicon.svg' | relative_url }}">
<link rel="alternate icon" href="{{ '/images/favicon.svg' | relative_url }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&family=Noto+Serif+SC:wght@400;500&family=Noto+Sans+SC:wght@400;500&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{background:#14110F;color:#E6DDCF;font-family:'Inter','Noto Sans SC',system-ui,sans-serif;-webkit-font-smoothing:antialiased;line-height:1.5;overflow-x:hidden}
/* stable, fixed oil-lamp photo behind the whole page (static — no per-frame repaint) */
body::before{content:'';position:fixed;inset:0;z-index:-3;pointer-events:none;background:url("{{ '/images/flame.jpg' | relative_url }}") 74% 55%/cover no-repeat}
/* fixed fine paper grain */
body::after{content:'';position:fixed;inset:0;z-index:-2;pointer-events:none;opacity:.05;mix-blend-mode:overlay;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");background-size:170px 170px}
a{color:#D8B27A;text-decoration:none;transition:color .3s ease,border-color .3s ease,opacity .3s ease}
a:hover{color:#E9C784}
::selection{background:rgba(224,173,88,.28);color:#F4DFC2}
*{scrollbar-width:thin;scrollbar-color:rgba(224,173,88,.30) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(224,173,88,.24);border-radius:8px;border:2px solid transparent;background-clip:padding-box}
::-webkit-scrollbar-thumb:hover{background:rgba(224,173,88,.44);background-clip:padding-box}
::-webkit-scrollbar-corner{background:transparent}
.serif{font-family:'Newsreader','Noto Serif SC',serif}
.mono{font-family:'IBM Plex Mono',monospace}
/* bilingual toggle — default Chinese; [data-lang=en] flips to English */
.i18n-en{display:none}
:root[data-lang="en"] .i18n-zh{display:none}
:root[data-lang="en"] .i18n-en{display:initial}
/* content sits over the fixed photo + a filter: bright in the hero, dimmed below */
.page{position:relative;min-height:100vh;background:linear-gradient(180deg, rgba(20,17,15,0) 0, rgba(20,17,15,0) 84vh, rgba(20,17,15,.82) 106vh)}
/* ember dot */
.ember{position:relative;display:inline-block;width:7px;height:7px;border-radius:50%;background:radial-gradient(circle,#FFF3DE,#E0AD58 60%,#C37D34);box-shadow:0 0 9px 2px rgba(224,173,88,.5);flex:none}
.ember-brand{width:9px;height:9px;box-shadow:0 0 10px 3px rgba(224,173,88,.6);animation:emberPulse 4.5s ease-in-out infinite}
@keyframes emberPulse{0%,100%{opacity:.55;transform:scale(1)}50%{opacity:1;transform:scale(1.25)}}
/* nav */
.nav{position:sticky;top:0;z-index:50;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:18px clamp(20px,5vw,64px);backdrop-filter:blur(10px);background:linear-gradient(180deg, rgba(20,17,15,.82), rgba(20,17,15,.35));border-bottom:1px solid rgba(224,173,88,.08)}
.brand{display:flex;align-items:center;gap:11px;color:#E6DDCF}
.brand:hover{color:#E6DDCF}
.brand-name{font-family:'Newsreader','Noto Serif SC',serif;font-size:16px;letter-spacing:.2px}
.navlinks{display:flex;align-items:center;gap:clamp(14px,2.6vw,30px)}
.navlinks a{font-size:13.5px;color:#AFA497}
.navlinks a:hover{color:#E6DDCF}
.lang-btn{cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:.5px;color:#E6DDCF;background:transparent;border:1px solid rgba(224,173,88,.28);border-radius:999px;padding:5px 12px;transition:border-color .3s,background .3s}
.lang-btn:hover{border-color:rgba(224,173,88,.6);background:rgba(224,173,88,.08)}
/* hero */
.hero{position:relative;min-height:92vh;display:flex;align-items:center;padding:clamp(24px,6vh,64px) clamp(24px,7vw,96px) clamp(60px,10vh,96px);overflow:hidden}
/* hero readability overlay (image itself is the fixed body background) */
.hero-photo{position:absolute;inset:0;z-index:0;pointer-events:none;background:linear-gradient(90deg, rgba(20,17,15,.92) 0%, rgba(20,17,15,.66) 34%, rgba(20,17,15,.22) 62%, rgba(20,17,15,.34) 100%),linear-gradient(180deg, rgba(20,17,15,.42) 0%, transparent 24%, transparent 74%, rgba(20,17,15,.22) 100%)}
/* a masked copy of the flame, warped by animated turbulence — makes the actual
flame tip lick and dance while the rest of the photo stays perfectly still. The
licking comes from the SMIL turbulence inside #flameWarp (a small region), so we
do NOT animate the CSS filter property here (that would re-run the whole filter). */
.flame-warp{position:fixed;inset:0;z-index:-3;pointer-events:none;background:url("{{ '/images/flame.jpg' | relative_url }}") 74% 55%/cover no-repeat;filter:url(#flameWarp) brightness(1.02);-webkit-mask:radial-gradient(3.5% 8% at 74% 56.5%, #000 0%, rgba(0,0,0,.5) 46%, transparent 78%);mask:radial-gradient(3.5% 8% at 74% 56.5%, #000 0%, rgba(0,0,0,.5) 46%, transparent 78%)}
/* fixed lamp light — the sole light source. A continuous graded falloff built from
overlapping elliptical layers (deep amber far field -> warm body -> bright inner ->
small white-hot heart) so there are no visible rings; two mid layers are warped by
different turbulence and flicker on their own clocks, easing the irregular inner
light smoothly out into the dark. Colour cools from white -> gold -> amber outward. */
.glow-far{position:fixed;inset:0;z-index:-1;pointer-events:none;mix-blend-mode:screen;background:radial-gradient(42% 62% at 74% 63%, rgba(198,102,44,.06) 0%, rgba(198,102,44,.028) 40%, transparent 78%);animation:breathe 11s ease-in-out infinite;will-change:opacity}
.glow-body{position:fixed;inset:0;z-index:-1;pointer-events:none;mix-blend-mode:screen;filter:url(#glowWarp);background:radial-gradient(24% 45% at 74% 61.5%, rgba(224,134,58,.085) 0%, transparent 76%),radial-gradient(15% 30% at 74% 60.5%, rgba(244,166,84,.12) 0%, transparent 76%);animation:pulseA 5.3s ease-in-out infinite;will-change:opacity}
.glow-mid{position:fixed;inset:0;z-index:-1;pointer-events:none;mix-blend-mode:screen;filter:url(#glowWarp2);background:radial-gradient(10.5% 22% at 74% 59.5%, rgba(255,188,108,.13) 0%, transparent 78%);animation:pulseB 3.7s ease-in-out infinite;will-change:opacity}
.glow-inner{position:fixed;inset:0;z-index:-1;pointer-events:none;mix-blend-mode:screen;background:radial-gradient(6.5% 14% at 74% 59%, rgba(255,214,150,.19) 0%, transparent 80%);transform-origin:74% 59%;animation:flickB 2.9s ease-in-out infinite reverse;will-change:opacity,transform}
.glow-core{position:fixed;inset:0;z-index:-1;pointer-events:none;mix-blend-mode:screen;background:radial-gradient(3.2% 7% at 74% 58.5%, rgba(255,244,218,.30) 0%, rgba(255,236,198,.14) 46%, transparent 82%);transform-origin:74% 58.5%;animation:flickCore 1.7s ease-in-out infinite;will-change:opacity,transform}
.lamp-haze{position:fixed;inset:0;z-index:-1;pointer-events:none;mix-blend-mode:screen;background:radial-gradient(7% 20% at 74% 48%, rgba(255,214,150,.09) 0%, transparent 72%);transform-origin:74% 54%;animation:haze 2.7s ease-in-out infinite;will-change:opacity,transform}
@keyframes breathe{0%,100%{opacity:.72}30%{opacity:.9}55%{opacity:.64}80%{opacity:.84}}
/* opacity-only flicker for the two turbulence-warped layers, so their (now static)
filter is rasterised once and only composited each frame — no per-frame recompute */
@keyframes pulseA{0%,100%{opacity:.8}14%{opacity:.9}27%{opacity:1}44%{opacity:.76}60%{opacity:.94}78%{opacity:.82}91%{opacity:.97}}
@keyframes pulseB{0%,100%{opacity:.68}12%{opacity:.9}25%{opacity:1}40%{opacity:.7}56%{opacity:.92}72%{opacity:.74}88%{opacity:.95}}
@keyframes flickB{0%{opacity:.7;transform:scaleY(1) translateX(0)}13%{opacity:.9;transform:scaleY(1.05) translateX(-.35%)}26%{opacity:1;transform:scaleY(1.09) translateX(-.15%)}39%{opacity:.68;transform:scaleY(.95) translateX(.28%)}53%{opacity:.9;transform:scaleY(1.04) translateX(.12%)}67%{opacity:.74;transform:scaleY(.99) translateX(-.2%)}81%{opacity:.95;transform:scaleY(1.06) translateX(-.08%)}93%{opacity:.72;transform:scaleY(1) translateX(.16%)}100%{opacity:.7;transform:scaleY(1) translateX(0)}}
@keyframes flickCore{0%,100%{opacity:.5;transform:translate(0,0) scaleY(1)}10%{opacity:.92;transform:translate(-.1%,-.2%) scaleY(1.18)}22%{opacity:1;transform:translate(.05%,-.34%) scaleY(1.28)}33%{opacity:.58;transform:translate(.1%,0) scaleY(.9)}45%{opacity:.9;transform:translate(-.06%,-.16%) scaleY(1.14)}58%{opacity:.64;transform:translate(.08%,-.03%) scaleY(1)}70%{opacity:.96;transform:translate(-.04%,-.24%) scaleY(1.22)}84%{opacity:.6;transform:translate(.06%,0) scaleY(.94)}}
@keyframes haze{0%{opacity:0;transform:translateY(0) scaleX(1)}22%{opacity:.5;transform:translateY(-1.4%) scaleX(.9)}46%{opacity:.32;transform:translateY(-2.6%) scaleX(1.12)}70%{opacity:.42;transform:translateY(-3.6%) scaleX(.86)}100%{opacity:0;transform:translateY(-4.8%) scaleX(1)}}
.hero-copy{position:relative;max-width:560px;z-index:2}
.kicker{display:flex;align-items:center;gap:11px;font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:2px;text-transform:uppercase;color:#8F857A;margin-bottom:26px}
.hero-name{font-family:'Newsreader','Noto Serif SC',serif;font-weight:400;font-size:clamp(46px,6.6vw,80px);line-height:1.04;letter-spacing:-.5px;color:#F0E8DA;text-wrap:balance}
.hero-slogan{display:flex;align-items:center;gap:15px;margin-top:22px;font-family:'Newsreader','Noto Serif SC',serif;font-style:italic;font-weight:400;font-size:clamp(20px,2.5vw,30px);line-height:1.3;letter-spacing:.2px;color:#D8B27A}
.hero-slogan::before{content:'';width:28px;height:1px;background:linear-gradient(90deg,#E0AD58,rgba(224,173,88,.12));flex:none}
.hero-sub{margin-top:26px;max-width:480px;font-size:clamp(15px,1.5vw,17.5px);line-height:1.72;color:#C7BCAB}
.hero-ctas{display:flex;flex-wrap:wrap;gap:14px 28px;margin-top:38px;align-items:center}
.cta-main{display:inline-flex;align-items:center;gap:9px;font-size:14.5px;color:#F4DFC2;border-bottom:1px solid rgba(224,173,88,.45);padding-bottom:4px}
.cta-main:hover{color:#F4DFC2;border-color:#E0AD58}
.cta-dot{width:6px;height:6px;border-radius:50%;background:#E0AD58;box-shadow:0 0 8px 2px rgba(224,173,88,.7);flex:none}
.cta-alt{font-size:14.5px;color:#AFA497}
.cta-alt:hover{color:#E6DDCF}
.hero-scroll{position:absolute;left:0;right:0;bottom:26px;display:flex;justify-content:center;pointer-events:none}
.hero-scroll span{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:2px;color:#6E655B}
/* wick divider */
.wick{display:flex;justify-content:center}
.wick div{width:1px;height:64px;background:linear-gradient(180deg,transparent,rgba(224,173,88,.4),transparent)}
.section{margin:0 auto;padding:clamp(48px,9vh,110px) clamp(24px,5vw,32px)}
.section h2{font-family:'Newsreader','Noto Serif SC',serif;font-weight:400;font-size:clamp(28px,3.6vw,44px);line-height:1.14;color:#F0E8DA;letter-spacing:-.3px;text-wrap:balance}
.section-intro{font-size:15px;color:#9E9488;margin-bottom:44px;max-width:560px}
.prose{margin-top:30px;font-size:17px;line-height:1.85;color:#D3C8B6;white-space:pre-line}
/* things I've found — findings list */
.finds{border-top:1px solid rgba(175,164,151,.1)}
.find-item{display:grid;grid-template-columns:172px 1fr;gap:clamp(20px,3vw,34px);align-items:start;padding:28px 0;border-bottom:1px solid rgba(175,164,151,.1)}
.find-fig{aspect-ratio:4/3;border-radius:5px;overflow:hidden;border:1px solid rgba(175,164,151,.14);background:#12100E;box-shadow:0 16px 40px -30px rgba(0,0,0,.8)}
.find-fig img{width:100%;height:100%;object-fit:cover;display:block}
.find-body{min-width:0}
.find-plain{font-family:'Newsreader','Noto Serif SC',serif;font-weight:400;font-size:clamp(18px,2vw,22px);line-height:1.5;color:#EAE0D0}
.find-tech{font-family:'Newsreader','Noto Serif SC',serif;font-style:italic;font-size:14px;color:#9FAFBA;margin-top:14px;line-height:1.5}
.find-meta{display:flex;gap:12px;align-items:baseline;flex-wrap:wrap;margin-top:11px;font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.4px}
.find-year{color:#E0AD58}
.find-venue{color:#8F857A}
.find-links{display:flex;gap:18px;margin-top:14px}
.find-links a{font-family:'IBM Plex Mono',monospace;font-size:12.5px;color:#AFA497}
.find-links a:hover{color:#E9C784}
.pub-all{display:inline-block;margin-top:32px;font-family:'IBM Plex Mono',monospace;font-size:13px;color:#AFA497}
.pub-all:hover{color:#E9C784}
/* building */
.build-item{padding:30px 0;border-top:1px solid rgba(175,164,151,.14)}
.build-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;flex-wrap:wrap}
.build-head h3{font-family:'IBM Plex Mono',monospace;font-weight:500;font-size:22px;letter-spacing:.3px;color:#F0E8DA}
.status{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.5px}
.status-active{color:#7F9A6E}
.status-early{color:#C0954C}
.build-item>p{margin-top:16px;font-size:16px;line-height:1.75;color:#D3C8B6;max-width:620px}
.build-link{display:inline-block;margin-top:16px;font-size:13.5px;color:#D8B27A}
.build-more{padding:28px 0;border-top:1px solid rgba(175,164,151,.14)}
.build-more p{font-size:14.5px;color:#8F857A;font-style:italic;font-family:'Newsreader','Noto Serif SC',serif}
/* notes timeline */
.timeline{position:relative;padding-left:28px}
.timeline::before{content:'';position:absolute;left:3px;top:6px;bottom:6px;width:1px;background:linear-gradient(180deg,rgba(224,173,88,.5),rgba(224,173,88,.12))}
.note-item{display:block;position:relative;padding:20px 0;border-bottom:1px solid rgba(175,164,151,.1)}
.note-item:last-of-type{border-bottom:none}
.note-dot{position:absolute;left:-28px;top:26px;width:7px;height:7px;border-radius:50%;background:#7A6E60;box-shadow:0 0 6px 1px rgba(175,164,151,.2)}
.note-item:nth-of-type(1) .note-dot{background:#E0AD58;box-shadow:0 0 8px 2px rgba(224,173,88,.5)}
.note-item:nth-of-type(2) .note-dot{background:#C37D34;box-shadow:0 0 8px 2px rgba(195,125,52,.4)}
.note-meta{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
.note-date,.note-time{font-family:'IBM Plex Mono',monospace;font-size:11px;color:#8F857A}
.note-time{color:#6E655B}
.chip{font-size:10.5px;letter-spacing:.5px;text-transform:uppercase;color:#9FAFBA;border:1px solid rgba(159,175,186,.25);border-radius:999px;padding:2px 9px}
.note-title{font-family:'Newsreader','Noto Serif SC',serif;font-size:21px;color:#EAE0D0;margin-top:10px;line-height:1.35}
.note-sub{font-size:14px;color:#9E9488;margin-top:7px;line-height:1.6}
.note-item:hover .note-title{color:#F4DFC2}
.all-notes{margin-top:36px}
.all-notes a{font-size:14px;color:#AFA497}
.all-notes a:hover{color:#E9C784}
/* about */
.about-grid{display:grid;grid-template-columns:.85fr 1.15fr;gap:clamp(28px,5vw,60px);align-items:start}
.about-photo{position:relative}
.about-photo .frame{aspect-ratio:4/5;border-radius:3px;overflow:hidden;border:1px solid rgba(175,164,151,.16);box-shadow:0 24px 60px -30px rgba(0,0,0,.7)}
.about-photo img{width:100%;height:100%;object-fit:cover;object-position:center;display:block;filter:saturate(.8) contrast(.97) brightness(.96)}
.about-photo::after{content:'';position:absolute;inset:0;border-radius:3px;pointer-events:none;background:radial-gradient(120% 80% at 30% 20%, rgba(224,173,88,.10), transparent 60%)}
.about-name{font-family:'Newsreader','Noto Serif SC',serif;font-weight:400;font-size:clamp(24px,2.8vw,32px);color:#F0E8DA;line-height:1.2}
.about-body{margin-top:20px;font-size:15.5px;line-height:1.8;color:#D3C8B6}
.about-meta{margin-top:22px;font-family:'IBM Plex Mono',monospace;font-size:12.5px;color:#8F857A;line-height:1.7;white-space:pre-line}
.about-cv{margin-top:16px}
.about-cv a{font-size:14px;color:#D8B27A}
/* contact */
.contact{text-align:center}
.contact-lamp{display:flex;justify-content:center;margin-bottom:34px}
.contact h2{font-style:italic;font-size:clamp(26px,3.4vw,40px);line-height:1.25}
.contact-body{margin:24px auto 0;max-width:520px;font-size:16px;line-height:1.75;color:#C7BCAB}
.contact-links{display:flex;flex-wrap:wrap;justify-content:center;gap:14px 26px;margin-top:40px}
.contact-links a{font-size:14.5px;color:#E6DDCF;border-bottom:1px solid rgba(224,173,88,.3);padding-bottom:3px}
.contact-links a:hover{color:#E6DDCF;border-color:#E0AD58}
/* footer */
.footer{border-top:1px solid rgba(224,173,88,.08);padding:34px clamp(24px,5vw,64px);display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
.footer p{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:#6E655B;letter-spacing:.5px}
.footer-cat{display:flex;align-items:center;gap:8px;opacity:.5}
.footer-cat span{font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:#5C5449}
/* reveal */
.reveal{opacity:0;transform:translateY(22px);transition:opacity 1.1s ease,transform 1.1s cubic-bezier(.2,.7,.2,1)}
.reveal.is-visible{opacity:1;transform:translateY(0)}
@media (max-width:860px){
.hero{min-height:82vh;padding-bottom:clamp(40px,8vh,72px)}
body::before{background-position:62% 42%}
.flame-warp{background-position:62% 42%;-webkit-mask:radial-gradient(19% 20% at 62% 40%, #000 0%, rgba(0,0,0,.72) 52%, transparent 80%);mask:radial-gradient(19% 20% at 62% 40%, #000 0%, rgba(0,0,0,.72) 52%, transparent 80%)}
.hero-photo{background:linear-gradient(180deg, rgba(20,17,15,.58) 0%, rgba(20,17,15,.3) 32%, rgba(20,17,15,.44) 70%, rgba(20,17,15,.26) 100%)}
.glow-far{background:radial-gradient(58% 56% at 62% 52%, rgba(198,102,44,.06) 0%, rgba(198,102,44,.028) 40%, transparent 78%)}
.glow-body{background:radial-gradient(34% 42% at 62% 49%, rgba(224,134,58,.085) 0%, transparent 76%),radial-gradient(22% 28% at 62% 48%, rgba(244,166,84,.12) 0%, transparent 76%);transform-origin:62% 49%}
.glow-mid{background:radial-gradient(15% 20% at 62% 47%, rgba(255,188,108,.13) 0%, transparent 78%);transform-origin:62% 47%}
.glow-inner{background:radial-gradient(9% 13% at 62% 46%, rgba(255,214,150,.19) 0%, transparent 80%);transform-origin:62% 46%}
.glow-core{background:radial-gradient(4.6% 6.6% at 62% 45.5%, rgba(255,244,218,.30) 0%, rgba(255,236,198,.14) 46%, transparent 82%);transform-origin:62% 45.5%}
.lamp-haze{background:radial-gradient(10% 17% at 62% 36%, rgba(255,214,150,.10) 0%, transparent 72%);transform-origin:62% 42%}
.about-grid{grid-template-columns:1fr}
.about-photo{max-width:360px}
.find-item{grid-template-columns:1fr;gap:16px}
.find-fig{max-width:240px}
.navlinks{gap:12px;flex-wrap:wrap;justify-content:flex-end}
.navlinks a{font-size:12px}
}
@media (prefers-reduced-motion: reduce){
html{scroll-behavior:auto}
.reveal{opacity:1;transform:none;transition:none}
.ember-brand{animation:none}
body::before,.flame-warp,.glow-far,.glow-body,.glow-mid,.glow-inner,.glow-core,.lamp-haze{animation:none}
.flame-warp,.glow-body,.glow-mid{filter:none}
}
</style>
<script>
/* set language before paint to avoid a flash of the wrong language in the nav */
(function () {
try {
var l = localStorage.getItem('lang') || (((navigator.language || '').toLowerCase().indexOf('zh') === 0) ? 'zh' : 'en');
if (l !== 'en' && l !== 'zh') l = 'en';
document.documentElement.setAttribute('data-lang', l);
} catch (e) { document.documentElement.setAttribute('data-lang', 'en'); }
})();
</script>
</head>
<body>
<!-- Turbulence warps. Only #flameWarp animates (the flame lick), and its region is
clipped to a small box around the flame so the per-frame noise is cheap. #glowWarp
and #glowWarp2 are STATIC (computed once) — they only give the cast light an
irregular, non-circular edge; the light's life comes from cheap opacity flicker. -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<filter id="flameWarp" x="0.6" y="0.42" width="0.28" height="0.36" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.022 0.06" numOctaves="1" seed="4" result="n">
<animate attributeName="baseFrequency" dur="7.5s" values="0.022 0.06;0.028 0.074;0.02 0.052;0.022 0.06" repeatCount="indefinite"/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="n" scale="4" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="glowWarp" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.006 0.011" numOctaves="2" seed="9" result="n2"/>
<feDisplacementMap in="SourceGraphic" in2="n2" scale="30" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="glowWarp2" x="-35%" y="-35%" width="170%" height="170%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.009 0.017" numOctaves="2" seed="2" result="n3"/>
<feDisplacementMap in="SourceGraphic" in2="n3" scale="22" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="flame-warp" aria-hidden="true"></div>
<div class="glow-far" aria-hidden="true"></div>
<div class="glow-body" aria-hidden="true"></div>
<div class="glow-mid" aria-hidden="true"></div>
<div class="glow-inner" aria-hidden="true"></div>
<div class="glow-core" aria-hidden="true"></div>
<div class="lamp-haze" aria-hidden="true"></div>
<div class="page">
{% include lamp-nav.html %}
<a id="top"></a>
<!-- HERO -->
<section class="hero">
<div class="hero-photo"></div>
<div class="hero-copy">
<h1 class="hero-name" data-i18n="heroName"></h1>
<p class="hero-slogan" data-i18n="heroTitle"></p>
<p class="hero-sub" data-i18n="heroSub"></p>
<div class="hero-ctas">
<a href="#building" class="cta-main"><span class="cta-dot"></span><span data-i18n="cta1"></span></a>
<a href="#notes" class="cta-alt"><span data-i18n="cta2"></span> →</a>
</div>
</div>
<div class="hero-scroll"><span data-i18n="scroll"></span></div>
</section>
<div class="wick"><div></div></div>
<!-- THE FIRST FLAME -->
<section class="section reveal" style="max-width:720px">
<p class="kicker"><span class="ember"></span><span data-i18n="s2kicker"></span></p>
<h2 data-i18n="s2title"></h2>
<p class="prose" data-i18n="s2body"></p>
</section>
<div class="wick"><div></div></div>
<!-- ESSAYS / NOTES BY THE LAMP -->
<a id="notes"></a>
<section class="section reveal" style="max-width:760px">
<p class="kicker" style="margin-bottom:14px"><span class="ember"></span><span data-i18n="s6kicker"></span></p>
<p class="section-intro" style="max-width:520px" data-i18n="s6intro"></p>
<div class="timeline">
{% assign essays = site.categories.essay %}
{% for post in essays limit:3 %}
<a class="note-item" href="{{ post.url | relative_url }}">
<span class="note-dot"></span>
<div class="note-meta">
<span class="note-date">{{ post.date | date: "%Y · %m" }}</span>
{% if post.tags and post.tags != empty %}<span class="chip">{{ post.tags | first }}</span>{% endif %}
<span class="note-time">{{ post.content | number_of_words | divided_by: 200 | plus: 1 }} min</span>
</div>
<p class="note-title">{% if post.title_en %}<span class="i18n-zh">{{ post.title | escape }}</span><span class="i18n-en">{{ post.title_en | escape }}</span>{% else %}{{ post.title | escape }}{% endif %}</p>
{% assign summary = post.excerpt | strip_html | normalize_whitespace %}
{% assign summary_en = post.excerpt_en | strip_html | normalize_whitespace %}
{% if summary and summary != "" %}<p class="note-sub">{% if post.excerpt_en %}<span class="i18n-zh">{{ summary | truncate: 120 }}</span><span class="i18n-en">{{ summary_en | truncate: 150 }}</span>{% else %}{{ summary | truncate: 120 }}{% endif %}</p>{% endif %}
</a>
{% else %}
<p class="note-sub" style="padding:20px 0" data-i18n="essaysEmpty"></p>
{% endfor %}
</div>
<p class="all-notes" style="display:flex;gap:26px;flex-wrap:wrap">
<a href="{{ '/essays/' | relative_url }}"><span data-i18n="allNotes"></span> →</a>
<a href="{{ '/study-notes/' | relative_url }}"><span data-i18n="studyNotesLink"></span> →</a>
</p>
</section>
<div class="wick"><div></div></div>
<!-- THINGS I'VE FOUND -->
<a id="found"></a>
<section class="section reveal" style="max-width:800px">
<p class="kicker" style="margin-bottom:14px"><span class="ember"></span><span data-i18n="s4kicker"></span></p>
<p class="section-intro" style="margin-bottom:46px" data-i18n="s4intro"></p>
<div class="finds">
<article class="find-item">
<div class="find-fig"><img src="{{ '/images/found/placeholder.svg' | relative_url }}" alt="Moiré phonon figure"></div>
<div class="find-body">
<p class="find-plain" data-i18n="found1plain"></p>
<p class="find-tech">Moiré Phonon Condensation in Magic-Angle Twisted Bilayer Graphene</p>
<div class="find-meta"><span class="find-year">2026</span><span class="find-venue">preprint</span></div>
<div class="find-links"><a href="https://arxiv.org/abs/2607.06711" target="_blank" rel="noopener">Paper ↗</a><a href="https://github.com/floatingCatty" target="_blank" rel="noopener">Code ↗</a></div>
</div>
</article>
<article class="find-item">
<div class="find-fig"><img src="{{ '/images/found/placeholder.svg' | relative_url }}" alt="SLEM figure"></div>
<div class="find-body">
<p class="find-plain" data-i18n="found2plain"></p>
<p class="find-tech">Learning local equivariant representations for quantum operators (SLEM)</p>
<div class="find-meta"><span class="find-year">2025</span><span class="find-venue">ICLR 2025</span></div>
<div class="find-links"><a href="https://arxiv.org/abs/2407.06053" target="_blank" rel="noopener">Paper ↗</a></div>
</div>
</article>
<article class="find-item">
<div class="find-fig"><img src="{{ '/images/found/placeholder.svg' | relative_url }}" alt="NQS impurity solver figure"></div>
<div class="find-body">
<p class="find-plain" data-i18n="found3plain"></p>
<p class="find-tech">Neural-Quantum-States Impurity Solver for Quantum Embedding Problems</p>
<div class="find-meta"><span class="find-year">2025</span><span class="find-venue">Phys. Rev. B (2026)</span></div>
<div class="find-links"><a href="https://arxiv.org/abs/2509.12431" target="_blank" rel="noopener">Paper ↗</a></div>
</div>
</article>
<article class="find-item">
<div class="find-fig"><img src="{{ '/images/found/placeholder.svg' | relative_url }}" alt="DeePTB figure"></div>
<div class="find-body">
<p class="find-plain" data-i18n="found4plain"></p>
<p class="find-tech">Deep learning tight-binding approach for large-scale electronic simulations (DeePTB)</p>
<div class="find-meta"><span class="find-year">2024</span><span class="find-venue">Nature Communications</span></div>
<div class="find-links"><a href="https://www.nature.com/articles/s41467-024-51006-4" target="_blank" rel="noopener">Paper ↗</a><a href="https://github.com/deepmodeling/DeePTB" target="_blank" rel="noopener">Code ↗</a></div>
</div>
</article>
<article class="find-item">
<div class="find-fig"><img src="{{ '/images/found/placeholder.svg' | relative_url }}" alt="AD-NEGF figure"></div>
<div class="find-body">
<p class="find-plain" data-i18n="found5plain"></p>
<p class="find-tech">Automatic differentiable nonequilibrium Green's function formalism (AD-NEGF)</p>
<div class="find-meta"><span class="find-year">2023</span><span class="find-venue">Phys. Rev. B</span></div>
<div class="find-links"><a href="https://doi.org/10.1103/PhysRevB.108.195143" target="_blank" rel="noopener">Paper ↗</a></div>
</div>
</article>
</div>
<a class="pub-all" href="{{ '/publications/' | relative_url }}"><span data-i18n="pubAll"></span> →</a>
</section>
<div class="wick"><div></div></div>
<!-- THINGS I'M BUILDING -->
<a id="building"></a>
<section class="section reveal" style="max-width:820px">
<p class="kicker" style="margin-bottom:14px"><span class="ember"></span><span data-i18n="s5kicker"></span></p>
<p class="section-intro" style="margin-bottom:48px" data-i18n="s5intro"></p>
<div>
<article class="build-item">
<div class="build-head"><h3>DeePTB</h3><span class="status status-active" data-i18n="statusActive"></span></div>
<p data-i18n="build1"></p>
<a class="build-link" href="https://github.com/deepmodeling/DeePTB" target="_blank" rel="noopener">github.com/deepmodeling/DeePTB ↗</a>
</article>
<article class="build-item">
<div class="build-head"><h3>VBCSR</h3><span class="status status-early" data-i18n="statusEarly"></span></div>
<p data-i18n="build2"></p>
<a class="build-link" href="https://github.com/DeepElectron/vbcsr" target="_blank" rel="noopener">github.com/DeepElectron/vbcsr ↗</a>
</article>
<div class="build-more"><p data-i18n="buildMore"></p></div>
</div>
</section>
<div class="wick"><div></div></div>
<!-- ABOUT -->
<a id="about"></a>
<section class="section reveal" style="max-width:900px">
<div class="about-grid">
<div class="about-photo">
<div class="frame"><img src="{{ '/images/profile.jpg?v=2' | relative_url }}" alt="Zhanghao Zhouyin"></div>
</div>
<div>
<p class="kicker" style="margin-bottom:20px"><span class="ember"></span><span data-i18n="s7kicker"></span></p>
<h2 class="about-name" data-i18n="aboutName"></h2>
<p class="about-body" data-i18n="aboutBody"></p>
<p class="about-meta" data-i18n="aboutMeta"></p>
<p class="about-cv"><a href="{{ '/cv/' | relative_url }}"><span data-i18n="cvLink"></span> ↗</a></p>
</div>
</div>
</section>
<div class="wick"><div></div></div>
<!-- CONTACT -->
<a id="contact"></a>
<section class="section contact reveal" style="max-width:720px;padding-bottom:clamp(48px,9vh,120px)">
<div class="contact-lamp"><canvas id="lamp-mini" style="width:120px;height:150px;display:block"></canvas></div>
<h2 data-i18n="contactTitle"></h2>
<p class="contact-body" data-i18n="contactBody"></p>
<div class="contact-links">
<a href="mailto:zhouyinzhanghao@gmail.com">Email</a>
<a href="https://github.com/floatingCatty" target="_blank" rel="noopener">GitHub</a>
<a href="https://scholar.google.com/citations?user=vSMmfvUAAAAJ" target="_blank" rel="noopener">Google Scholar</a>
<a href="https://www.linkedin.com/in/zhanghao-zhouyin-4aaa471a5/" target="_blank" rel="noopener">LinkedIn</a>
</div>
</section>
<!-- FOOTER -->
<footer class="footer">
<p>Zhanghao Zhouyin · McGill University · <span data-i18n="footer"></span></p>
<div class="footer-cat">
<svg width="26" height="18" viewBox="0 0 26 18" fill="none">
<path d="M4 15c0-3 2-5 5-5s5 2 5 5" stroke="#8F857A" stroke-width="1" fill="none"/>
<path d="M6 10.5l-1.5-3 3 1.2M12 10.5l1.5-3-3 1.2" stroke="#8F857A" stroke-width="1" fill="none" stroke-linejoin="round"/>
<circle cx="7.4" cy="12.5" r=".7" fill="#8F857A"/><circle cx="10.6" cy="12.5" r=".7" fill="#8F857A"/>
<circle cx="20" cy="14" r="1.3" fill="#6E655B"/><circle cx="23" cy="12.5" r="1.3" fill="#6E655B"/>
</svg>
<span>still by the lamp</span>
</div>
</footer>
</div>
<script>
(function () {
'use strict';
/* ---------------- i18n ---------------- */
var I18N = {
en: {
brand: 'Keep a light on for curiosity',
navWork: 'Work', navNotes: 'Essays', navAbout: 'About', navContact: 'Contact', langLabel: '中文',
heroName: 'Zhanghao Zhouyin',
heroTitle: 'Keep a light on for curiosity.',
heroSub: 'I work between physics and AI, trying to understand a little more of the world — and to keep a light on for others who want to know too.',
cta1: "What I'm building", cta2: 'Notes by the lamp',
scroll: 'scroll',
s2kicker: 'The first flame',
s2title: 'It began with a small flame.',
s2body: "In my grandmother's house, a rush-wick oil lamp always burned before the family altar — a shallow brass dish, a little cooking oil, a flame the size of a soybean, yet it almost never went out. Over the years the oil turned to grease and the grease to a waxy crust on the dish, with a faint smell like a candle. I would sit beside it — trimming the wick, watching the flame swell and shrink, burning small things: a stick of incense, a scrap of paper, a strand of hair, an ant I had caught.\n\nBack then the play had no purpose; it was only curiosity. Other motives are mixed in now, but the heart of it hasn't changed. CS, AI, physics — underneath them all is still the same simple wish to understand the world.",
s4kicker: "Things I've found",
s4intro: 'Finished research and discoveries.',
found1plain: 'A structural change involving more than ten thousand atoms turned out to be governed by just two collective motions.',
found1tech: 'Moiré Phonon Condensation in Magic-Angle Twisted Bilayer Graphene',
found2plain: 'A neural network can read only a material’s local structure and predict its quantum operators directly — reaching near first-principles accuracy while staying local enough to scale toward whole devices.',
found3plain: 'Neural quantum states can act as the impurity solver inside quantum embedding, matching exact diagonalization on correlated models — where the real bottleneck is how precisely you sample observables, not training the network.',
found4plain: 'Trained only on first-principles eigenvalues, a deep-learning tight-binding model predicts the electronic structure of structures it has never seen — making million-atom, finite-temperature electronic simulations feasible.',
found5plain: 'Making the non-equilibrium Green’s function method differentiable end to end turns a quantum transport simulator into something you can optimize through — providing gradients for sensitivity analysis and inverse design.',
pubAll: 'All publications',
s5kicker: "What I'm building",
s5intro: 'Building tools that help people understand the world.',
statusActive: 'open source · active', statusEarly: 'open source · early',
build1: 'A deep-learning tight-binding framework that predicts electronic structure at close to first-principles accuracy, for a fraction of the cost — built for researchers who need large-scale quantum simulations without a supercomputer’s budget.',
build2: 'Infrastructure for representing and computing with very large sparse operators in electronic-structure and quantum-transport work — the quiet plumbing that lets bigger simulations run on smaller machines.',
buildMore: 'And a few things still forming — AI-native DFT, scientific agents, and the beginnings of new project around.',
s6kicker: 'Essays',
s6intro: 'Thinking out loud, in progress — on discovery, building, AI and science, and the way there.',
allNotes: 'All essays',
studyNotesLink: 'Study notes',
essaysEmpty: 'The first essay is on its way.',
s7kicker: 'About',
aboutName: 'Zhanghao Zhouyin',
aboutBody: 'I trained early in computer science and electronic engineering, then followed a stubborn question — what is the physical world behind all this technology? — into materials, quantum transport, and computational physics. Today I work where physics, machine learning, and scientific computing meet. Lately I’ve been trying to make AI agents that don’t just organize references or write code, but genuinely take part: proposing hypotheses, designing calculations, finding hidden structure. Whether it becomes research, software, or something I build, the question stays the same — can we make better scientific tools, so that individuals and small teams can understand complex things, test their ideas, and create.',
aboutMeta: 'PhD candidate, Physics · McGill University\nContributor: DeePTB · DFTio · AD-NEGF · VBCSR',
cvLink: 'Full CV & history',
contactTitle: 'Some questions are better explored together.',
contactBody: 'I’m always glad to talk about scientific tools, AI for discovery, and ambitious problems worth building around.',
footer: '© 2026'
},
zh: {
brand: '为好奇心留一盏灯',
navWork: '工作', navNotes: '随笔', navAbout: '关于', navContact: '联系', langLabel: 'EN',
heroName: '周寅张皓',
heroTitle: '为好奇心留一盏灯。',
heroSub: '我在物理和 AI 之间,试着多理解一点这个世界,也为同样想知道的人点一盏灯。',
cta1: '在做的东西', cta2: '灯下笔记',
scroll: '向下',
s2kicker: '最初的火',
s2title: '从一小簇火开始。',
s2body: '小时候,奶奶家佛桌前总点着一盏灯心草油灯。一个浅浅黄铜托盘,盛点做饭用的菜油,黄豆大小的火苗,但几乎从不熄灭。几十年,油变脂,脂变膏,黏在托盘上,有像蜡烛一样的味道。我常坐在它旁边,拨一拨灯芯,看火焰忽大忽小,烧一点小东西。有时候是供香,纸片,头发,抓的小蚂蚁。\n\n那时候的玩耍无关收益,只是好奇。现在夹杂了很多别的想法,但是本心不改。CS,AI,物理,底下藏着的还是一个想要理解世界的、简单的心愿。',
s4kicker: '我发现的一些事',
s4intro: '已完成的研究与发现。',
found1plain: '一个涉及上万个原子的结构变化,最终被证明只由两种集体运动所主导。',
found1tech: 'Moiré Phonon Condensation in Magic-Angle Twisted Bilayer Graphene',
found2plain: '让物理对称性和局域性引导神经网络,直接预测 DFT 中的量子算符:局域性让神经网络模拟器件尺度材料成为可能。',
found3plain: '探索神经网络求解多体量子材料,对于强关联体系,NQS真正的瓶颈在于物理量采样的精度。',
found4plain: '深度学习紧束缚模型,用AI让百万原子、有限温度下的电子模拟成为可能。',
found5plain: '让非平衡格林函数方法端到端可微,让量子器件的设计优化更自动,更精确。',
pubAll: '全部论文',
s5kicker: '在做的东西',
s5intro: '建造一些工具,帮助人理解世界。',
statusActive: '开源 · 持续开发', statusEarly: '开源 · 早期',
build1: '一个深度学习紧束缚框架,用接近第一性原理的精度预测电子结构,成本却低得多——为那些需要做大规模量子模拟、却没有超算预算的研究者而建。',
build2: '一套用于表示和计算超大稀疏算子的基础设施,服务于电子结构与量子输运计算——是那些看不见的底层管道,让更大的模拟能在更小的机器上跑起来。',
buildMore: '还有一些仍在成形之中——AI-native DFT、科学智能体,以及一个正在酝酿计划。',
s6kicker: '随笔',
s6intro: '一边想一边写,仍在进行中——关于发现、建造、AI 与科学,以及沿途的思考。',
allNotes: '全部随笔',
studyNotesLink: '学习笔记',
essaysEmpty: '第一篇随笔正在路上。',
s7kicker: '关于',
aboutName: '周寅张皓',
aboutBody: '我早期学习计算机与人工智能,后来因为一个固执的问题——技术背后的物理世界到底是什么样子——一步步走向材料、量子模拟与计算物理。今天,我工作在物理、化学、AI和科学计算的交界处。我用AI和物理理论建造一些工具,让我们能够探索更大,更多样的物质世界;也在用这些工具探索和挖掘未知的现象,描述和预测和优化真实世界的材料、器件。探索边界的过程让我激动,也想让每个团队,每个人都可以拥有理解复杂世界、验证想法并创造新事物的能力。',
aboutMeta: '物理学博士候选人 · 麦吉尔大学\n开源贡献:DeePTB · DFTio · AD-NEGF · VBCSR',
cvLink: '完整 CV 与经历',
contactTitle: '有些问题,一起探索更好。',
contactBody: '我们可以聊聊科学、社会、AI,以及那些值得长期投入去建造的宏大问题。',
footer: '© 2026'
}
};
function applyLang(lang) {
var t = I18N[lang] || I18N.en;
document.querySelectorAll('[data-i18n]').forEach(function (el) {
var key = el.getAttribute('data-i18n');
if (t[key] !== undefined) el.textContent = t[key];
});
document.getElementById('lang-toggle').textContent = t.langLabel;
document.documentElement.lang = lang === 'zh' ? 'zh-Hans' : 'en';
document.documentElement.setAttribute('data-lang', lang);
try { localStorage.setItem('lang', lang); } catch (e) {}
currentLang = lang;
}
var currentLang = 'en';
try {
currentLang = localStorage.getItem('lang') ||
((navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en');
} catch (e) {}
if (currentLang !== 'en' && currentLang !== 'zh') currentLang = 'en';
applyLang(currentLang);
document.getElementById('lang-toggle').addEventListener('click', function () {
applyLang(currentLang === 'en' ? 'zh' : 'en');
});
/* ---------------- motion prefs & scroll intensity ---------------- */
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var heroIntensity = 1;
if (!reduce) {
window.addEventListener('scroll', function () {
var p = Math.min(1, window.scrollY / (window.innerHeight * 1.2));
heroIntensity = 1 + p * 0.07;
}, { passive: true });
}
/* ---------------- reveal on scroll ---------------- */
if (!reduce && 'IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { en.target.classList.add('is-visible'); io.unobserve(en.target); }
});
}, { threshold: 0.14 });
document.querySelectorAll('.reveal').forEach(function (el) { io.observe(el); });
} else {
document.querySelectorAll('.reveal').forEach(function (el) { el.classList.add('is-visible'); });
}
/* ---------------- oil-lamp canvas ---------------- */
function startLamp(id, big) {
var canvas = document.getElementById(id);
if (!canvas) return;
var ctx = canvas.getContext('2d');
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var w = 0, h = 0;
function resize() {
var r = canvas.getBoundingClientRect();
w = r.width; h = r.height;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener('resize', function () { resize(); if (reduce) draw(0); });
// Stable mottled deposits on the oil surface (seeded once, reused each frame)
var seed = big ? 20240717 : 90311;
var rnd = function () { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
var mottle = [];
for (var i = 0; i < 22; i++) {
var a = rnd() * Math.PI * 2, rad = Math.sqrt(rnd()) * 0.9;
var kind = rnd();
mottle.push({
u: Math.cos(a) * rad, v: Math.sin(a) * rad * 0.9,
r: 0.08 + rnd() * 0.22,
light: kind > 0.5,
a: 0.10 + rnd() * 0.22
});
}
function draw(t) {
var flick = reduce ? 0.5
: (0.5 + 0.30 * Math.sin(t * 0.0022) + 0.13 * Math.sin(t * 0.0069) + 0.08 * Math.sin(t * 0.016));
var sway = reduce ? 0 : (0.05 * Math.sin(t * 0.0016) + 0.03 * Math.sin(t * 0.0065));
var inten = big ? heroIntensity : 1;
var cx = w * 0.5;
var dishCY = h * (big ? 0.70 : 0.72);
var rx = w * (big ? 0.40 : 0.42);
var ry = w * (big ? 0.105 : 0.115);
var poolCX = cx, poolCY = dishCY - ry * 0.10;
var poolRx = rx * 0.80, poolRy = ry * 0.62;
var wickX = cx + rx * 0.40;
var surfaceY = poolCY - poolRy * 0.30;
var baseH = h * (big ? 0.20 : 0.24);
var fH = baseH * (0.90 + 0.12 * flick) * (0.85 + 0.15 * inten);
var fW = w * (big ? 0.052 : 0.075);
var swx = fW * sway * 2.4;
var flameBottom = surfaceY - ry * 0.28;
ctx.clearRect(0, 0, w, h);
// ambient glow: warm near the flame, fading smoothly into the background tone
ctx.save();
var gcx = wickX, gcy = flameBottom - fH * 0.35;
var gr = Math.min(w * 0.40, h * 0.34) * (1 + 0.05 * flick) * (0.94 + 0.06 * inten);
var glow = ctx.createRadialGradient(gcx, gcy, 0, gcx, gcy, gr);
glow.addColorStop(0, 'rgba(226,150,80,' + (0.19 * inten) + ')');
glow.addColorStop(0.2, 'rgba(190,112,52,' + (0.09 * inten) + ')');
glow.addColorStop(0.46, 'rgba(140,82,42,' + (0.032 * inten) + ')');
glow.addColorStop(1, 'rgba(26,23,20,0)');
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = glow;
ctx.fillRect(0, 0, w, h);
ctx.restore();
// plate body (ceramic dish, lit from the flame side)
ctx.save();
var body = ctx.createRadialGradient(wickX, dishCY - ry * 0.2, ry * 0.2, cx, dishCY, rx * 1.15);
body.addColorStop(0, '#c9a26a');
body.addColorStop(0.35, '#8a6a44');
body.addColorStop(0.7, '#37281c');
body.addColorStop(1, '#14100c');
ctx.fillStyle = body;
ctx.beginPath();
ctx.ellipse(cx, dishCY, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
var edge = ctx.createLinearGradient(0, dishCY, 0, dishCY + ry * 1.5);
edge.addColorStop(0, 'rgba(120,88,54,0.55)');
edge.addColorStop(1, 'rgba(12,9,7,0)');
ctx.fillStyle = edge;
ctx.beginPath();
ctx.ellipse(cx, dishCY, rx, ry, 0, 0.05 * Math.PI, 0.95 * Math.PI, false);
ctx.ellipse(cx, dishCY + ry * 0.55, rx * 0.98, ry * 1.1, 0, 0.95 * Math.PI, 0.05 * Math.PI, true);
ctx.fill();
ctx.restore();
// oil pool (clipped)
ctx.save();
ctx.beginPath();
ctx.ellipse(poolCX, poolCY, poolRx, poolRy, 0, 0, Math.PI * 2);
ctx.clip();
var oil = ctx.createLinearGradient(0, poolCY - poolRy, 0, poolCY + poolRy);
oil.addColorStop(0, '#241a11');
oil.addColorStop(1, '#3a2a19');
ctx.fillStyle = oil;
ctx.fillRect(poolCX - poolRx, poolCY - poolRy, poolRx * 2, poolRy * 2);
ctx.globalCompositeOperation = 'lighter';
var lit = ctx.createRadialGradient(wickX, surfaceY, 0, wickX, surfaceY, poolRx * 1.5);
lit.addColorStop(0, 'rgba(228,186,124,' + (0.58 * inten) + ')');
lit.addColorStop(0.3, 'rgba(200,144,80,' + (0.32 * inten) + ')');
lit.addColorStop(0.7, 'rgba(150,100,52,0.16)');
lit.addColorStop(1, 'rgba(120,80,40,0)');
ctx.fillStyle = lit;
ctx.fillRect(poolCX - poolRx, poolCY - poolRy, poolRx * 2, poolRy * 2);
mottle.forEach(function (m) {
var mx = poolCX + m.u * poolRx, my = poolCY + m.v * poolRy;
var mr = m.r * poolRx;
var g = ctx.createRadialGradient(mx, my, 0, mx, my, mr);
if (m.light) {
ctx.globalCompositeOperation = 'lighter';
g.addColorStop(0, 'rgba(246,222,178,' + m.a + ')');
g.addColorStop(1, 'rgba(246,222,178,0)');
} else {
ctx.globalCompositeOperation = 'source-over';
g.addColorStop(0, 'rgba(30,20,12,' + m.a + ')');
g.addColorStop(1, 'rgba(30,20,12,0)');
}
ctx.fillStyle = g;
ctx.beginPath();
ctx.ellipse(mx, my, mr, mr * 0.7, 0, 0, Math.PI * 2);
ctx.fill();
});
// reflection of the flame smeared down the oil toward the viewer
ctx.globalCompositeOperation = 'lighter';
var refl = ctx.createLinearGradient(wickX, surfaceY, wickX - poolRx * 0.3, poolCY + poolRy);
refl.addColorStop(0, 'rgba(255,224,150,' + (0.5 * inten) + ')');
refl.addColorStop(1, 'rgba(255,224,150,0)');
ctx.fillStyle = refl;
ctx.beginPath();
ctx.ellipse(wickX - poolRx * 0.12, surfaceY + poolRy * 0.5, poolRx * 0.16, poolRy * 0.9, -0.12, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// pool edge: dark inner shade all around + bright lit lip on the flame side
ctx.save();
ctx.strokeStyle = 'rgba(18,12,8,0.6)';
ctx.lineWidth = Math.max(1, ry * 0.14);
ctx.beginPath();
ctx.ellipse(poolCX, poolCY, poolRx, poolRy, 0, 0, Math.PI * 2);
ctx.stroke();
var rimGrad = ctx.createLinearGradient(cx - rx, 0, cx + rx, 0);
rimGrad.addColorStop(0, 'rgba(190,150,98,0.18)');
rimGrad.addColorStop(0.45, 'rgba(224,188,132,' + (0.5 * inten) + ')');
rimGrad.addColorStop(1, 'rgba(238,212,160,' + (0.72 * inten) + ')');
ctx.strokeStyle = rimGrad;
ctx.lineWidth = Math.max(1.2, ry * 0.26);
ctx.beginPath();
ctx.ellipse(cx, dishCY, rx * 0.995, ry * 0.995, 0, 0.12 * Math.PI, 0.88 * Math.PI, false);
ctx.stroke();
ctx.strokeStyle = 'rgba(120,92,58,0.18)';
ctx.lineWidth = Math.max(1, ry * 0.1);
ctx.beginPath();
ctx.ellipse(cx, dishCY, rx * 0.99, ry * 0.99, 0, 1.08 * Math.PI, 1.92 * Math.PI, false);
ctx.stroke();
ctx.restore();
// wick: a short rush head barely poking above the oil
ctx.save();
ctx.strokeStyle = '#3a2a1a';
ctx.lineWidth = big ? 3.2 : 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(wickX - fW * 0.15, surfaceY + ry * 0.05);
ctx.quadraticCurveTo(wickX, surfaceY - ry * 0.12, wickX + swx * 0.4, flameBottom + fH * 0.06);
ctx.stroke();
var tipG = ctx.createRadialGradient(wickX + swx * 0.4, flameBottom, 0, wickX + swx * 0.4, flameBottom, fW * 0.8);
tipG.addColorStop(0, 'rgba(255,236,196,0.95)');
tipG.addColorStop(0.5, 'rgba(226,150,70,0.55)');
tipG.addColorStop(1, 'rgba(226,150,70,0)');
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = tipG;
ctx.beginPath();
ctx.arc(wickX + swx * 0.4, flameBottom, fW * 0.8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// flame
ctx.save();
ctx.globalCompositeOperation = 'lighter';
var fx = wickX, tipX = wickX + swx;
var flame = function (scale, grad, tipLift) {
var bw = fW * scale, hh = fH * scale;
ctx.beginPath();
ctx.moveTo(fx, flameBottom);
ctx.bezierCurveTo(fx - bw, flameBottom - hh * 0.32, fx - bw * 0.4 + swx * 0.5, flameBottom - hh * 0.7, tipX, flameBottom - hh * tipLift);
ctx.bezierCurveTo(fx + bw * 0.4 + swx * 0.5, flameBottom - hh * 0.7, fx + bw, flameBottom - hh * 0.32, fx, flameBottom);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
};
var bl = ctx.createRadialGradient(fx, flameBottom - fH * 0.05, 0, fx, flameBottom - fH * 0.05, fW * 0.9);
bl.addColorStop(0, 'rgba(120,140,180,0.4)');
bl.addColorStop(1, 'rgba(120,140,180,0)');
ctx.fillStyle = bl;
ctx.beginPath();
ctx.ellipse(fx, flameBottom - fH * 0.03, fW * 0.7, fH * 0.12, 0, 0, Math.PI * 2);
ctx.fill();
var og = ctx.createLinearGradient(0, flameBottom, 0, flameBottom - fH);
og.addColorStop(0, 'rgba(224,148,68,0.46)');
og.addColorStop(0.4, 'rgba(222,134,52,0.6)');
og.addColorStop(0.8, 'rgba(190,102,42,0.34)');
og.addColorStop(1, 'rgba(174,92,38,0)');
flame(1, og, 1.0);
var ig = ctx.createLinearGradient(0, flameBottom, 0, flameBottom - fH * 0.7);
ig.addColorStop(0, 'rgba(252,224,174,0.72)');
ig.addColorStop(0.5, 'rgba(244,192,118,0.66)');
ig.addColorStop(1, 'rgba(236,174,86,0)');
flame(0.52, ig, 0.86);
var core = ctx.createRadialGradient(fx, flameBottom - fH * 0.14, 0, fx, flameBottom - fH * 0.14, fW * 0.5);
core.addColorStop(0, 'rgba(255,238,202,0.7)');
core.addColorStop(1, 'rgba(255,236,206,0)');
ctx.fillStyle = core;
ctx.beginPath();
ctx.ellipse(fx, flameBottom - fH * 0.14, fW * 0.34, fH * 0.2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
if (!reduce) requestAnimationFrame(draw);
}
if (reduce) draw(0);
else requestAnimationFrame(draw);
}
startLamp('lamp-mini', false);
})();
</script>
{% include lamp-embers.html %}
</body>
</html>