1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>kejun@github:~$</title> 7 <link rel="preconnect" href="https://fonts.googleapis.com"> 8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> 10 11 <!-- Three.js for 3D Models --> 12
12<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
12 13 <!-- fflate - Required by FBXLoader --> 14
14<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js"></script>
14 15
15<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
15 16
16<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/FBXLoader.js"></script>
16 17
17<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
17 18 <style> 19 * { 20 margin: 0; 21 padding: 0; 22 box-sizing: border-box; 23 } 24 25 :root { 26 --bg-color: #0d1117; 27 --terminal-bg: #161b22; 28 --text-primary: #e6edf3; 29 --text-secondary: #7d8590; 30 --accent-green: #238636; 31 --accent-blue: #58a6ff; 32 --accent-yellow: #d29922; 33 --accent-red: #f85149; 34 --accent-purple: #a371f7; 35 --accent-cyan: #39c5cf; 36 --border-color: #30363d; 37 } 38 39 body { 40 font-family: 'JetBrains Mono', 'Courier New', monospace; 41 background-color: var(--bg-color); 42 color: var(--text-primary); 43 line-height: 1.6; 44 min-height: 100vh; 45 overflow-x: hidden; 46 } 47 48 /* Terminal Window */ 49 .terminal { 50 max-width: 1200px; 51 margin: 20px auto; 52 background: var(--terminal-bg); 53 border-radius: 10px; 54 border: 1px solid var(--border-color); 55 overflow: hidden; 56 box-shadow: 0 20px 50px rgba(0,0,0,0.5); 57 } 58 59 /* Terminal Header */ 60 .terminal-header { 61 background: linear-gradient(180deg, #2d333b 0%, #1c2128 100%); 62 padding: 12px 16px; 63 display: flex; 64 align-items: center; 65 gap: 8px; 66 border-bottom: 1px solid var(--border-color); 67 } 68 69 .terminal-btn { 70 width: 12px; 71 height: 12px; 72 border-radius: 50%; 73 border: none; 74 cursor: pointer; 75 } 76 77 .btn-close { background: #ff5f56; } 78 .btn-minimize { background: #ffbd2e; } 79 .btn-maximize { background: #27c93f; } 80 81 .terminal-title { 82 flex: 1; 83 text-align: center; 84 font-size: 13px; 85 color: var(--text-secondary); 86 } 87 88 /* Terminal Body */ 89 .terminal-body { 90 padding: 20px; 91 min-height: 600px; 92 } 93 94 /* ASCII Art */ 95 .ascii-art { 96 color: var(--accent-green); 97 font-size: 11px; 98 line-height: 1.2; 99 margin-bottom: 20px; 100 white-space: pre; 101 overflow-x: auto; 102 } 103 104 /* Command Line */ 105 .command-line { 106 display: flex; 107 align-items: flex-start; 108 margin-bottom: 15px; 109 flex-wrap: wrap; 110 } 111 112 .prompt { 113 color: var(--accent-green); 114 margin-right: 10px; 115 white-space: nowrap; 116 } 117 118 .command { 119 color: var(--text-primary); 120 } 121 122 .cursor { 123 display: inline-block; 124 width: 10px; 125 height: 20px; 126 background: var(--accent-green); 127 animation: blink 1s infinite; 128 vertical-align: middle; 129 margin-left: 2px; 130 } 131 132 @keyframes blink { 133 0%, 50% { opacity: 1; } 134 51%, 100% { opacity: 0; } 135 } 136 137 /* Output Sections */ 138 .output { 139 margin: 20px 0; 140 padding-left: 20px; 141 } 142 143 .section-title { 144 color: var(--accent-blue); 145 font-weight: bold; 146 margin-bottom: 10px; 147 display: flex; 148 align-items: center; 149 gap: 10px; 150 } 151 152 .section-title::before { 153 content: 'â¶'; 154 color: var(--accent-green); 155 } 156 157 /* Info Cards */ 158 .info-grid { 159 display: grid; 160 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 161 gap: 20px; 162 margin: 20px 0; 163 } 164 165 .info-card { 166 background: rgba(22, 27, 34, 0.8); 167 border: 1px solid var(--border-color); 168 border-radius: 8px; 169 padding: 20px; 170 transition: all 0.3s ease; 171 } 172 173 .info-card:hover { 174 border-color: var(--accent-blue); 175 transform: translateY(-2px); 176 } 177 178 .card-header { 179 display: flex; 180 align-items: center; 181 gap: 10px; 182 margin-bottom: 15px; 183 color: var(--accent-yellow); 184 } 185 186 .card-header .icon { 187 font-size: 20px; 188 } 189 190 .card-content { 191 color: var(--text-secondary); 192 } 193 194 .card-content strong { 195 color: var(--text-primary); 196 } 197 198 /* Stats */ 199 .stats-row { 200 display: flex; 201 gap: 30px; 202 flex-wrap: wrap; 203 margin: 15px 0; 204 } 205 206 .stat-item { 207 display: flex; 208 align-items: center; 209 gap: 8px; 210 } 211 212 .stat-value { 213 color: var(--accent-green); 214 font-weight: bold; 215 font-size: 24px; 216 } 217 218 .stat-label { 219 color: var(--text-secondary); 220 font-size: 14px; 221 } 222 223 /* Repository List */ 224 .repo-list { 225 list-style: none; 226 } 227 228 .repo-item { 229 padding: 15px; 230 border-left: 3px solid var(--border-color); 231 margin-bottom: 10px; 232 background: rgba(22, 27, 34, 0.5); 233 transition: all 0.3s ease; 234 } 235 236 .repo-item:hover { 237 border-left-color: var(--accent-green); 238 background: rgba(35, 134, 54, 0.1); 239 } 240 241 .repo-name { 242 color: var(--accent-blue); 243 font-weight: bold; 244 text-decoration: none; 245 display: flex; 246 align-items: center; 247 gap: 10px; 248 } 249 250 .repo-name:hover { 251 text-decoration: underline; 252 } 253 254 .repo-desc { 255 color: var(--text-secondary); 256 margin-top: 5px; 257 font-size: 14px; 258 } 259 260 .repo-stats { 261 display: flex; 262 gap: 20px; 263 margin-top: 10px; 264 font-size: 13px; 265 color: var(--text-secondary); 266 } 267 268 .repo-stats span { 269 display: flex; 270 align-items: center; 271 gap: 5px; 272 } 273 274 /* Skills */ 275 .skills-container { 276 display: flex; 277 flex-wrap: wrap; 278 gap: 10px; 279 margin-top: 15px; 280 } 281 282 .skill-tag { 283 background: rgba(88, 166, 255, 0.1); 284 color: var(--accent-blue); 285 padding: 5px 12px; 286 border-radius: 20px; 287 font-size: 13px; 288 border: 1px solid rgba(88, 166, 255, 0.3); 289 } 290 291 /* Links */ 292 .link-list { 293 display: flex; 294 flex-wrap: wrap; 295 gap: 15px; 296 margin-top: 15px; 297 } 298 299 .link-item { 300 color: var(--accent-cyan); 301 text-decoration: none; 302 display: flex; 303 align-items: center; 304 gap: 8px; 305 padding: 8px 15px; 306 border: 1px solid var(--border-color); 307 border-radius: 6px; 308 transition: all 0.3s ease; 309 } 310 311 .link-item:hover { 312 background: rgba(57, 197, 207, 0.1); 313 border-color: var(--accent-cyan); 314 } 315
316 /* Typing Effect */ 317 .typing-text { 318 overflow: hidden; 319 white-space: nowrap; 320 animation: typing 3s steps(40, end); 321 } 322 323 @keyframes typing { 324 from { width: 0; } 325 to { width: 100%; } 326 } 327 328 /* Command Menu */ 329 .command-menu { 330 position: fixed; 331 bottom: 20px; 332 left: 50%; 333 transform: translateX(-50%); 334 background: var(--terminal-bg); 335 border: 1px solid var(--border-color); 336 border-radius: 8px; 337 padding: 10px 20px; 338 display: flex; 339 gap: 15px; 340 z-index: 100; 341 } 342 343 .menu-btn { 344 background: none; 345 border: 1px solid var(--border-color); 346 color: var(--text-secondary); 347 padding: 8px 16px; 348 border-radius: 6px; 349 cursor: pointer; 350 font-family: inherit; 351 transition: all 0.3s ease; 352 } 353 354 .menu-btn:hover { 355 border-color: var(--accent-green); 356 color: var(--accent-green); 357 } 358 359 /* Scrollbar */ 360 ::-webkit-scrollbar {
361 width: 10px; 362 height: 10px; 363 } 364 365 ::-webkit-scrollbar-track { 366 background: var(--bg-color); 367 } 368 369 ::-webkit-scrollbar-thumb { 370 background: var(--border-color); 371 border-radius: 5px; 372 } 373 374 ::-webkit-scrollbar-thumb:hover { 375 background: var(--text-secondary); 376 } 377 378 /* Responsive */ 379 @media (max-width: 768px) { 380 .terminal { 381 margin: 10px; 382 } 383 384 .ascii-art { 385 font-size: 8px; 386 } 387 388 .info-grid { 389 grid-template-columns: 1fr; 390 } 391 392 .command-menu { 393 flex-wrap: wrap; 394 width: 90%; 395 justify-content: center; 396 } 397 } 398 399 /* Hidden sections */ 400 .section { 401 display: none; 402 } 403 404 .section.active { 405 display: block; 406 animation: fadeIn 0.5s ease; 407 } 408 409 @keyframes fadeIn { 410 from { opacity: 0; transform: translateY(10px); } 411 to { opacity: 1; transform: translateY(0); } 412 } 413 414 /* Progress Bar */ 415 .progress-bar { 416 width: 100%; 417 height: 6px; 418 background: var(--border-color); 419 border-radius: 3px; 420 overflow: hidden; 421 margin-top: 5px; 422 } 423 424 .progress-fill { 425 height: 100%; 426 background: linear-gradient(90deg, var(--accent-green), var(--accent-blue)); 427 border-radius: 3px; 428 transition: width 1s ease; 429 } 430 </style> 431</head> 432<body> 433 <div class="terminal"> 434 <div class="terminal-header"> 435 <div class="terminal-btn btn-close"></div> 436 <div class="terminal-btn btn-minimize"></div> 437 <div class="terminal-btn btn-maximize"></div> 438 <div class="terminal-title">kejun@github: ~ (zsh)</div> 439 </div> 440 441 <div class="terminal-body"> 442 <!-- ASCII Art --> 443 <pre class="ascii-art"> 444 âââ âââââââââââ ââââââ âââââââ âââ 445 âââ ââââââââââââ ââââââ ââââââââ âââ 446 âââââââ ââââââ ââââââ âââââââââ âââ 447 âââââââ ââââââ ââ ââââââ âââââââââââââ 448 âââ âââââââââââââââââââââââââââââââ ââââââ 449 âââ âââââââââââ ââââââ âââââââ âââ âââââ 450 </pre> 451 452 <!-- Welcome Message --> 453 <div class="command-line"> 454 <span class="prompt">â</span> 455 <span class="command">whoami<span class="cursor"></span></span> 456 </div> 457 458 <div class="output"> 459 <p style="color: var(--accent-cyan); font-size: 18px; margin-bottom: 10px;"> 460 ð Hi, I'm <strong style="color: var(--text-primary);">å å (kejun)</strong> 461 </p> 462 <p style="color: var(--text-secondary);"> 463 Frontend Developer | Database Enthusiast | Open Source Contributor 464 </p> 465 <p style="color: var(--text-secondary); margin-top: 10px;"> 466 ð Beijing, China | Exploring AI-Native Database & Modern Web Dev 467 </p> 468 </div> 469 470 <!-- Command Navigation --> 471 <div class="command-line" style="margin-top: 30px;"> 472 <span class="prompt">â</span> 473 <span class="command">ls -la sections/</span> 474 </div> 475 476 <div class="output"> 477 <div style="display: flex; gap: 20px; flex-wrap: wrap;"> 478 <button class="menu-btn" onclick="showSection('about')">about/</button> 479 <button class="menu-btn" onclick="showSection('projects')">projects/</button> 480 <button class="menu-btn" onclick="showSection('skills')">skills/</button> 481 <button class="menu-btn" onclick="showSection('stats')">stats/</button> 482 <button class="menu-btn" onclick="showSection('contact')">contact/</button> 483 </div> 484 </div> 485 486 <!-- External Links --> 487 <div class="command-line" style="margin-top: 10px;">
488 <span class="prompt">â</span> 489 <span class="command">ls -la links/</span> 490 </div> 491 492 <div class="output"> 493 <div style="display: flex; gap: 20px; flex-wrap: wrap;"> 494 <a href="https://github.com/kejun/blogpost/blob/main/README.md" class="link-item" target="_blank"> 495 <span>ð</span> Blog Posts 496 </a> 497 <a href="https://github.com/kejun/daily-investor/tree/main/ideas" class="link-item" target="_blank"> 498 <span>ð¡</span> Ideas 499 </a> 500 <a href="https://github.com/kejun/daily-rss/tree/main" class="link-item" target="_blank"> 501 <span>ð¡</span> Daily RSS 502 </a> 503 </div> 504 </div> 505 506 <!-- About Section --> 507 <div id="about" class="section active"> 508 <div class="command-line"> 509 <span class="prompt">â</span> 510 <span class="command">cat about.md</span> 511 </div> 512 <div class="output"> 513 <div class="info-grid"> 514 <div class="info-card"> 515 <div class="card-header"> 516 <span class="icon">ð¼</span> 517 <span>Work</span> 518 </div> 519 <div class="card-content"> 520 <p>Frontend Developer passionate about modern web technologies</p> 521 <p style="margin-top: 10px;">Exploring AI-Native Database & Vector Search applications</p> 522 </div> 523 </div> 524 <div class="info-card"> 525 <div class="card-header"> 526 <span class="icon">ð¯</span> 527 <span>Focus</span> 528 </div> 529 <div class="card-content"> 530 <p>⢠Frontend Architecture</p> 531 <p>⢠JavaScript/TypeScript</p> 532 <p>⢠Database Systems</p> 533 <p>⢠AI Infrastructure</p> 534 </div> 535 </div> 536 <div class="info-card"> 537 <div class="card-header"> 538 <span class="icon">ð</span> 539 <span>Highlights</span> 540 </div> 541 <div class="card-content"> 542 <p>⢠Creator of <strong>Do</strong> (291â)</p> 543 <p>⢠Contributor to <strong>OceanBase</strong></p> 544 <p>⢠Open source enthusiast since 2010</p> 545 </div> 546 </div> 547 </div> 548 </div> 549 </div> 550 551 <!-- Projects Section --> 552 <div id="projects" class="section"> 553 <div class="command-line"> 554 <span class="prompt">â</span> 555 <span class="command">gh repo list --limit 10</span> 556 </div> 557 <div class="output"> 558 <ul class="repo-list"> 559 <li class="repo-item"> 560 <a href="https://github.com/kejun/Do" class="repo-name" target="_blank"> 561 â¡ Do 562 </a> 563 <p class="repo-desc">è½»é级æä»¶å è½½åä¾èµç®¡çè§£å³æ¹æ¡</p> 564 <div class="repo-stats"> 565 <span>â 291 stars</span> 566 <span>ð´ 71 forks</span> 567 <span>ð¨ JavaScript</span> 568 </div> 569 </li> 570 <li class="repo-item"> 571 <a href="https://github.com/kejun/CSS-Code
571-Guideline" class="repo-name" target="_blank"> 572 ð CSS Code Guideline 573 </a> 574 <p class="repo-desc">CSS ç¼ç è§è䏿佳å®è·µ</p> 575 <div class="repo-stats"> 576 <span>â 130 stars</span> 577 <span>ð´ 26 forks</span> 578 </div> 579 </li> 580 <li class="repo-item"> 581 <a href="https://github.com/kejun/Douban-JSLint" class="repo-name" target="_blank"> 582 ð Douban JSLint 583 </a> 584 <p class="repo-desc">JavaScript ä»£ç æ£æ¥å·¥å ·</p> 585 <div class="repo-stats"> 586 <span>â 41 stars</span> 587 <span>ð´ 3 forks</span> 588 <span>ð¨ JavaScript</span> 589 </div> 590 </li> 591 <li class="repo-item"> 592 <a href="https://github.com/kejun/mcp-oceanbase" class="repo-name" target="_blank"> 593 ð mcp-oceanbase 594 </a> 595 <p class="repo-desc">MCP Server for OceanBase database and its tools</p> 596 <div class="repo-stats"> 597 <span>ð§ Apache-2.0</span> 598 </div> 599 </li> 600 </ul> 601 </div> 602 </div> 603 604 <!-- Skills Section --> 605 <div id="skills" class="section"> 606 <div class="command-line"> 607 <span class="prompt">â</span> 608 <span class="command">cat skills.json</span> 609 </div> 610 <div class="output"> 611 <div class="info-card" style="margin-bottom: 20px;"> 612 <div class="card-header"> 613 <span class="icon">ð»</span> 614 <span>Languages</span> 615 </div> 616 <div class="skills-container"> 617 <span class="skill-tag">JavaScript</span> 618 <span class="skill-tag">TypeScript</span> 619 <span class="skill-tag">C++</span> 620 <span class="skill-tag">Python</span> 621 <span class="skill-tag">SQL</span> 622 </div> 623 </div> 624 625 <div class="info-card" style="margin-bottom: 20px;"> 626 <div class="card-header"> 627 <span class="icon">ðï¸</span> 628 <span>Database & Search</span> 629 </div> 630 <div class="skills-container"> 631 <span class="skill-tag">OceanBase</span> 632 <span class="skill-tag">seekdb</span> 633 <span class="skill-tag">Vector Search</span> 634 <span class="skill-tag">Hybrid Search</span> 635 <span class="skill-tag">RAG</span> 636 </div> 637 </div> 638 639 <div class="info-card"> 640 <div class="card-header"> 641 <span class="icon">ð ï¸</span> 642 <span>Tools & Frameworks</span> 643 </div> 644 <div class="skills-container"> 645 <span class="skill-tag">Node.js</span> 646 <span class="skill-tag">React</span> 647 <span class="skill-tag">LangChain</span> 648 <span class="skill-tag">MCP</span> 649 <span class="skill-tag">Git</span> 650 </div> 651 </div> 652 </div> 653 </div> 654 655 <!-- Stats Section --> 656 <div id="stats" class="section"> 657 <div class="command-line">
658 <span class="prompt">â</span> 659 <span class="command">gh status</span> 660 </div> 661 <div class="output"> 662 <div class="stats-row"> 663 <div class="stat-item"> 664 <span class="stat-value">45</span> 665 <span class="stat-label">Public Repos</span> 666 </div> 667 <div class="stat-item"> 668 <span class="stat-value">75</span> 669 <span class="stat-label">Gists</span> 670 </div> 671 <div class="stat-item"> 672 <span class="stat-value">500+</span> 673 <span class="stat-label">Total Stars</span> 674 </div> 675 </div> 676 677 <div class="info-card" style="margin-top: 30px;"> 678 <div class="card-header"> 679 <span class="icon">ð</span> 680 <span>GitHub Stats</span> 681 </div> 682 <div style="margin-top: 20px;"> 683 <div style="margin-bottom: 15px;"> 684 <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> 685 <span>JavaScript</span> 686 <span style="color: var(--accent-green);">45%</span> 687 </div> 688 <div class="progress-bar"> 689 <div class="progress-fill" style="width: 45%;"></div> 690 </div> 691 </div> 692 <div style="margin-bottom: 15px;"> 693 <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> 694 <span>TypeScript</span> 695 <span style="color: var(--accent-blue);">25%</span> 696 </div> 697 <div class="progress-bar"> 698 <div class="progress-fill" style="width: 25%;"></div> 699 </div> 700 </div> 701 <div style="margin-bottom: 15px;"> 702 <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> 703 <span>C++</span> 704 <span style="color: var(--accent-yellow);">20%</span> 705 </div> 706 <div class="progress-bar"> 707 <div class="progress-fill" style="width: 20%;"></div> 708 </div> 709 </div> 710 <div> 711 <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> 712 <span>Others</span> 713 <span style="color: var(--accent-purple);">10%</span> 714 </div> 715 <div class="progress-bar"> 716 <div class="progress-fill" style="width: 10%;"></div> 717 </div> 718 </div> 719 </div> 720 </div> 721 </div> 722 </div> 723 724 <!-- Contact Section --> 725 <div id="contact" class="section"> 726 <div class="command-line"> 727 <span class="prompt">â</span> 728 <span class="command">cat contact.txt</span> 729 </div> 730 <div class="output"> 731 <div class="info-card"> 732 <div class="card-header"> 733 <span class="icon">ð§</span>
734 <span>Connect with me</span> 735 </div> 736 <div class="link-list" style="margin-top: 20px;"> 737 <a href="https://github.com/kejun" class="link-item" target="_blank"> 738 <span>ð</span> GitHub 739 </a> 740 <a href="http://weibo.com/kejunz" class="link-item" target="_blank"> 741 <span>ð±</span> Weibo 742 </a> 743 <a href="mailto:[email protected]" class="link-item"> 744 <span>âï¸</span> Email 745 </a> 746 <a href="https://www.oceanbase.ai" class="link-item" target="_blank"> 747 <span>ð</span> OceanBase 748 </a> 749 </div> 750 </div> 751 752 <div class="command-line" style="margin-top: 30px;"> 753 <span class="prompt">â</span> 754 <span class="command">echo "Let's build something amazing together!"</span> 755 </div> 756 <div class="output"> 757 <p style="color: var(--accent-green); font-size: 16px; margin-top: 10px;"> 758 ð Let's build something amazing together! 759 </p> 760 </div> 761 </div> 762 </div> 763 764 <!-- Footer --> 765 <div class="command-line" style="margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border-color);"> 766 <span class="prompt">â</span> 767 <span class="command" style="color: var(--text-secondary);"> 768 Last login: <span id="last-login"></span> from <span style="color: var(--accent-cyan);">Beijing, China</span> 769 </span> 770 </div> 771 </div> 772 </div> 773 774
774<script> 775 // Show current time 776 document.getElementById('last-login').textContent = new Date().toLocaleString('zh-CN'); 777 778 // Section navigation 779 function showSection(sectionId) { 780 // Hide all sections 781 const sections = document.querySelectorAll('.section'); 782 sections.forEach(section => { 783 section.classList.remove('active'); 784 }); 785 786 // Show selected section 787 const targetSection = document.getElementById(sectionId); 788 if (targetSection) { 789 targetSection.classList.add('active'); 790 } 791 792 // Update command line 793 const commandLine = targetSection.previousElementSibling; 794 if (commandLine && commandLine.classList.contains('command-line')) { 795 const command = commandLine.querySelector('.command'); 796 if (command) { 797 command.innerHTML = command.textContent + '<span class="cursor"></span>'; 798 } 799 } 800 } 801 802 // Typing effect for welcome message 803 const welcomeText = "whoami"; 804 const welcomeCommand = document.querySelector('.command'); 805 let charIndex = 0; 806 807 function typeWelcome() { 808 if (charIndex < welcomeText.length) { 809 welcomeCommand.innerHTML = welcomeText.substring(0, charIndex + 1) + '<span class="cursor"></span>'; 810 charIndex++; 811 setTimeout(typeWelcome, 100); 812 } 813 } 814 815 // Start typing effect on load 816 window.addEventListener('load', () => { 817 welcomeCommand.innerHTML = '<span class="cursor"></span>'; 818 charIndex = 0; 819 setTimeout(typeWelcome, 500); 820 }); 821 822 // Keyboard shortcuts 823 document.addEventListener('keydown', (e) => { 824 if (e.key === '1') showSection('about'); 825 if (e.key === '2') showSection('projects'); 826 if (e.key === '3') showSection('skills'); 827 if (e.key === '4') showSection('stats'); 828 if (e.key === '5') showSection('contact'); 829 }); 830 831 // Animate progress bars when stats section is shown 832 const observer = new IntersectionObserver((entries) => { 833 entries.forEach(entry => { 834 if (entry.isIntersecting) { 835 const progressBars = entry.target.querySelectorAll('.progress-fill'); 836 progressBars.forEach(bar => { 837 const width = bar.style.width; 838 bar.style.width = '0'; 839 setTimeout(() => { 840 bar.style.width = width; 841 }, 100); 842 }); 843 } 844 }); 845 }); 846 847 const statsSection = document.getElementById('stats'); 848 if (statsSection) { 849 observer.observe(statsSection); 850 } 851 852 // Easter egg: Konami code 853 let konamiCode = []; 854 const konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; 855 856 document.addEventListener('keydown', (e) => { 857 konamiCode.push(e.key); 858 konamiCode = konamiCode.slice(-10); 859 860 if (konamiCode.join(',') === konamiSequence.join(',')) { 861 document.body.style.animation = 'rainbow 2s infinite'; 862 setTimeout(() => { 863 document.body.style.animation = ''; 864 }, 5000); 865 } 866 }); 867 868 // Add rainbow animation 869 const style = document.createElement('style'); 870 style.textContent = ` 871 @keyframes rainbow { 872 0% { filter: hue-rotate(0deg); } 873 100% { filter: hue-rotate(360deg); } 874 } 875 `; 876 document.head.appendChild(style); 877 878 // ========== AI Companion 3D Robot ========== 879 const aiCompanionCSS = ` 880 #ai-companion-canvas{position:fixed;bottom:20px;right:20px;z-index:999999;width:340px;height:50vh;min-height:360px;max-height:calc(100vh - 40px);border-radius:16px;overflow:hidden;pointer-events:none;background:transparent;transition:transform 0.4s cubic-bezier(0.4,0,0.2,1),opacity 0.4s cubic-bezier(0.4,0,0.2,1)} 881 #ai-companion-canvas.collapsed{transform:translateY(calc(100% + 20px));opacity:0;pointer-events:none} 882 #ai-companion-canvas canvas{width:100%;height:100%;display:block} 883 #ai-companion-toggle{position:fixed;bottom:24px;right:24px;z-index:1000000;width:40px;height:40px;border-radius:50%;border:1px solid rgba(48,54,61,0.8);background:rgba(22,27,34,0.85);backdrop-filter:blur(8px);color:#e6edf3;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;line-height:1;transition:background 0.2s,transform 0.2s;pointer-events:auto;box-shadow:0 2px 8px rgba(0,0,0,0.3)} 884 #ai-companion-toggle:hover{background:rgba(35,134,54,0.6);transform:scale(1.1)} 885 `; 886 887 // æ·»å æ ·å¼ 888 const aiStyle = document.createElement('style'); 889 aiStyle.textContent = aiCompanionCSS; 890 document.head.appendChild(aiStyle); 891 892 // æ·»å HTML ç»æ 893 const aiHTML = `<div id="ai-companion-canvas" class="collapsed"></div><button id="ai-companion-toggle" title="å±å¼3D人ç©">ð¤</button>`; 894 document.body.insertAdjacentHTML('beforeend', aiHTML); 895 896 // æ¶èµ·/å±å¼æé®é»è¾ 897 (function() { 898 const toggleBtn = document.getElementById('ai-companion-toggle'); 899 const companionCanvas = document.getElementById('ai-companion-canvas'); 900 let isCollapsed = true; 901 902 toggleBtn.addEventListener('click', function() { 903 isCollapsed = !isCollapsed; 904 if (isCollapsed) { 905 companionCanvas.classList.add('collapsed'); 906 toggleBtn.textContent = 'ð¤'; 907 toggleBtn.title = 'å±å¼3D人ç©'; 908 } else { 909 companionCanvas.classList.remove('collapsed'); 910 toggleBtn.textContent = 'â¼'; 911 toggleBtn.title = 'æ¶èµ·3D人ç©'; 912 } 913 }); 914 })(); 915 916 // AI Companion 3D Logic 917 (function() { 918 const AI_COMPANION_VERSION = 'v2026.02.14.5'; 919 const canvas = document.getElementById('ai-companion-canvas'); 920 const CONFIG = { width: 340 }; 921 922 // è·åå½åç»å¸é«åº¦ 923 function getCanvasHeight() { 924 const preferredHeight = Math.max(320, window.innerHeight * 0.45); 925 return Math.max(220, Math.min(preferredHeight, window.innerHeight - 40)); 926 } 927 928 let currentHeight = getCanvasHeight(); 929 let model, mixer, actions = {}, currentAnimation = 'Walking', clock = new THREE.Clock(); 930 931 // é¼ æ è·éæè½¬åé 932 let targetRotationY = 0; 933 const MAX_ROTATION_RADIANS = 40 * (Math.PI / 180); // 40度ï¼å·¦å³å40åº¦ï¼æ»å ±80度 934 const ROTATION_SMOOTHING_FACTOR = 0.05; // æè½¬å¹³æ»æå¼ç³»æ° 935 936 // çä»è£¤åºååæ°ï¼åºäºæ¨¡åå½ä¸åé«åº¦ 1.5ï¼ 937 const PANTS_BOTTOM_Y = 0.10; // èè¸ä½ç½® 938 const PANTS_TOP_Y = 0.55; // è °é¨ä½ç½® 939 const PANTS_BLEND_EDGE = 0.03; // è¿æ¸¡åºå宽度 940 941 const scene = new THREE.Scene(); 942 scene.background = null; 943 944 const camera = new THREE.PerspectiveCamera(45, CONFIG.width / currentHeight, 0.1, 1000); 945 camera.position.set(0, 2, 6); 946 947 const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 948 renderer.setSize(CONFIG.width, currentHeight); 949 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 950 renderer.shadowMap.enabled = true; 951 renderer.shadowMap.type = THREE.PCFSoftShadowMap; 952 renderer.outputEncoding = THREE.sRGBEncoding; 953 renderer.toneMapping = THREE.ACESFilmicToneMapping; 954 renderer.toneMappingExposure = 1.0; 955 renderer.setClearColor(0x000000, 0); 956 canvas.appendChild(renderer.domElement); 957 958 // OrbitControls - ç¦ç¨æè½¬ï¼æ¹ç¨é¼ æ è·é 959 const controls = new THREE.OrbitControls(camera, renderer.domElement); 960 controls.enablePan = false;
961 controls.enableZoom = false; 962 controls.enableRotate = false; // ç¦ç¨æè½¬åè½ 963 controls.minPolarAngle = Math.PI / 4; 964 controls.maxPolarAngle = Math.PI / 2; 965 controls.target.set(0, 0.8, 0); 966 967 // ç¯å - æåèªç¶å ç §ï¼åå°å¡ææ 968 const hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x080820, 0.6); 969 scene.add(hemiLight); 970 971 const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); 972 scene.add(ambientLight); 973 974 const dirLight = new THREE.DirectionalLight(0xfff5e6, 0.8); 975 dirLight.position.set(5, 10, 5); 976 dirLight.castShadow = true; 977 scene.add(dirLight); 978 979 const fillLight = new THREE.DirectionalLight(0xe8f0ff, 0.3); 980 fillLight.position.set(-3, 5, 3); 981 scene.add(fillLight); 982 983 const backLight = new THREE.DirectionalLight(0x58a6ff, 0.3); 984 backLight.position.set(-5, 5, -5); 985 scene.add(backLight); 986 987 const loader = new THREE.FBXLoader(); 988 989 function loadModel(url) { 990 return new Promise((resolve, reject) => { 991 loader.load( 992 url, 993 (object) => { 994 console.log(`â Loaded: ${url}`); 995 resolve(object); 996 }, 997 (xhr) => { 998 console.log(`${(xhr.loaded / xhr.total * 100).toFixed(1)}% loaded`); 999 }, 1000 (error) => { 1001 console.error('â FBXå 载失败:', error); 1002 reject(error); 1003 } 1004 ); 1005 }); 1006 } 1007 1008 // è·å模å宿´å å´ç - éåææ mesh å¹¶åºç¨ä¸ç忢 1009 function getCompleteModelBounds(object) { 1010 const box = new THREE.Box3(); 1011 object.updateMatrixWorld(true); 1012 1013 object.traverse(child => { 1014 if (child.isMesh && child.geometry) { 1015 // 计ç®å ä½ä½çå å´ç 1016 child.geometry.computeBoundingBox(); 1017 1018 if (child.geometry.boundingBox) { 1019 // å° mesh æ¬å°å å´ç忢å°ä¸çåæ 1020 const gbox = child.geometry.boundingBox.clone(); 1021 gbox.applyMatrix4(child.matrixWorld); 1022 box.union(gbox); 1023 } 1024 } 1025 }); 1026 1027 // éªè¯å å´çæ¯å¦ææ 1028 if (!box.isEmpty()) { 1029 return box; 1030 } 1031 1032 // å¦ææ²¡ææ¾å°ææç meshï¼è¿åä¸ä¸ªé»è®¤çå å´ç 1033 console.warn('â ï¸ No valid meshes found, using fallback bounding box'); 1034 return new THREE.Box3().setFromObject(object); 1035 } 1036 1037 // å建ç¨åºåçä»å¸çº¹ç 1038 function createDenimTexture() { 1039 const canvas = document.createElement('canvas'); 1040 canvas.width = 256; 1041 canvas.height = 256; 1042 const ctx = canvas.getContext('2d'); 1043 1044 // åºç¡çä»èè² 1045 ctx.fillStyle = '#2b4570'; 1046 ctx.fillRect(0, 0, 256, 256); 1047 1048 // æ·»å çä»å¸çº¤ç»´çº¹çï¼æçº¹ï¼ 1049 ctx.strokeStyle = 'rgba(60, 90, 140, 0.4)'; 1050 ctx.lineWidth = 1; 1051 for (let i = -256; i < 512; i += 3) { 1052 ctx.beginPath(); 1053 ctx.moveTo(i, 0); 1054 ctx.lineTo(i + 256, 256); 1055 ctx.stroke(); 1056 } 1057 1058 // ååæçº¹ï¼äº¤åç¼ç»ææï¼ 1059 ctx.strokeStyle = 'rgba(35, 55, 90, 0.3)'; 1060 for (let i = -256; i < 512; i += 4) { 1061 ctx.beginPath(); 1062 ctx.moveTo(i + 256, 0); 1063 ctx.lineTo(i, 256); 1064 ctx.stroke(); 1065 } 1066 1067 // æ·»å éæºåªç¹æ¨¡æå¸æè´¨æ 1068 const imageData = ctx.getImageData(0, 0, 256, 256); 1069 for (let i = 0; i < imageData.data.length; i += 4) { 1070 const noise = (Math.random() - 0.5) * 15; 1071 imageData.data[i] += noise; 1072 imageData.data[i + 1] += noise; 1073 imageData.data[i + 2] += noise; 1074 } 1075 ctx.putImageData(imageData, 0, 0); 1076
1077 const texture = new THREE.CanvasTexture(canvas); 1078 texture.wrapS = THREE.RepeatWrapping; 1079 texture.wrapT = THREE.RepeatWrapping; 1080 texture.repeat.set(4, 4); 1081 return texture; 1082 } 1083 1084 // è°æ´æ¨¡åï¼ç¼©æ¾ãå± ä¸ãæè´¨å¤çï¼ 1085 function setupModel(object) { 1086 // å è®¡ç®ææmeshçå ä½å å´ç 1087 object.traverse((child) => { 1088 if (child.isMesh && child.geometry) { 1089 child.geometry.computeBoundingBox(); 1090 child.geometry.computeBoundingSphere(); 1091 } 1092 }); 1093 1094 object.updateMatrixWorld(true); 1095 1096 // 使ç¨å®æ´çå å´çè®¡ç®æ¹æ³ 1097 const box = getCompleteModelBounds(object); 1098 const size = box.getSize(new THREE.Vector3()); 1099 const center = box.getCenter(new THREE.Vector3()); 1100 1101 console.log(`ð Model bounds: ${size.x.toFixed(2)} x ${size.y.toFixed(2)} x ${size.z.toFixed(2)}`); 1102 console.log(`ð Model center: ${center.x.toFixed(2)}, ${center.y.toFixed(2)}, ${center.z.toFixed(2)}`); 1103 1104 const targetHeight = 1.5; 1105 const scale = targetHeight / size.y; 1106 object.scale.setScalar(scale); 1107 1108 object.updateMatrixWorld(true); 1109 1110 // éæ°è®¡ç®ç¼©æ¾åçå å´ç 1111 const scaledBox = getCompleteModelBounds(object); 1112 const scaledCenter = scaledBox.getCenter(new THREE.Vector3()); 1113 1114 // å± ä¸ x å z è½´ 1115 object.position.x -= scaledCenter.x; 1116 object.position.z -= scaledCenter.z; 1117 1118 // å°èåºæ¾ç½®å¨ç»å¸åºé¨ï¼y = 0.05ï¼ç¨å¾®ç¦»å¼åºé¨è¾¹ç¼ï¼ 1119 const desiredY = 0.05; 1120 object.position.y = object.position.y - (scaledBox.min.y - desiredY); 1121 1122 // å建çä»è£¤çº¹ç 1123 const denimTexture = createDenimTexture(); 1124 1125 object.traverse((child) => { 1126 if (child.isMesh) { 1127 child.castShadow = true; 1128 child.receiveShadow = true; 1129 if (child.material) { 1130 child.material.side = THREE.DoubleSide; 1131 // åå°å¡ææï¼å¢å ç²ç³åº¦ï¼éä½éå±åº¦åé«å 1132 if (child.material.roughness !== undefined) { 1133 child.material.roughness = Math.max(child.material.roughness, 0.7); 1134 } 1135 if (child.material.metalness !== undefined) { 1136 child.material.metalness = Math.min(child.material.metalness, 0.1); 1137 } 1138 if (child.material.shininess !== undefined) { 1139 child.material.shininess = Math.min(child.material.shininess, 30); 1140 } 1141 if (child.material.specular) { 1142 child.material.specular.setHex(0x222222); 1143 } 1144 1145 // å°è£¤ååºåè´´å¾æ¹ä¸ºçä»è£¤ææ 1146 // ä½¿ç¨ onBeforeCompile æ³¨å ¥èªå®ä¹çè²å¨ï¼æ ¹æ®ä¸çåæ Y 弿··åçä»å¸çº¹ç 1147 child.material.onBeforeCompile = (shader) => { 1148 shader.uniforms.denimMap = { value: denimTexture }; 1149 shader.uniforms.pantsBottom = { value: PANTS_BOTTOM_Y }; 1150 shader.uniforms.pantsTop = { value: PANTS_TOP_Y }; 1151 shader.uniforms.blendEdge = { value: PANTS_BLEND_EDGE }; 1152 1153 // é¡¶ç¹çè²å¨ï¼ä¼ éä¸çåæ Yï¼æ¯æéª¨éª¼å¨ç»ï¼ 1154 shader.vertexShader = shader.vertexShader.replace( 1155 'void main() {', 1156 'varying float vWorldY;\nvoid main() {' 1157 ); 1158 shader.vertexShader = shader.vertexShader.replace( 1159 '#include <project_vertex>', 1160 'vWorldY = (modelMatrix * vec4(transformed, 1.0)).y;\n#include <project_vertex>' 1161 ); 1162 1163 // çæ®µçè²å¨ï¼å¨è£¤ååºåæ··åçä»å¸é¢è² 1164 shader.fragmentShader = shader.fragmentShader.replace( 1165 'void main() {', 1166 'uniform sampler2D denimMap;\nuniform float pantsBottom;\nuniform float pantsTop;\nuniform float blendEdge;\nvarying float vWorldY;\nvoid main() {' 1167 ); 1168 shader.fragmentShader = shader.fragmentShader.replace( 1169 '#include <dithering_fragment>', 1170 `// çä»è£¤è´´å¾æ··å 1171 float bottomEdge = smoothstep(pantsBottom - blendEdge, pantsBottom + blendEdge, vWorldY); 1172 float topEdge = 1.0 - smoothstep(pantsTop - blendEdge, pantsTop + blendEdge, vWorldY);
1173 float pantsMask = bottomEdge * topEdge; 1174 #ifdef USE_UV 1175 vec2 denimUV = vUv * 4.0; 1176 #else 1177 vec2 denimUV = gl_FragCoord.xy / 64.0; 1178 #endif 1179 vec3 denimColor = texture2D(denimMap, denimUV).rgb; 1180 gl_FragColor.rgb = mix(gl_FragColor.rgb, denimColor, pantsMask * 0.85); 1181 #include <dithering_fragment>` 1182 ); 1183 }; 1184 child.material.needsUpdate = true; 1185 } 1186 } 1187 }); 1188 1189 object.updateMatrixWorld(true); 1190 1191 // æç»å å´çè®¡ç® 1192 const finalBox = getCompleteModelBounds(object); 1193 const finalSize = finalBox.getSize(new THREE.Vector3()); 1194 const modelHeight = finalSize.y; 1195 const footY = finalBox.min.y; // èåºä½ç½® 1196 const headY = finalBox.max.y; // 头顶ä½ç½® 1197 const centerX = (finalBox.min.x + finalBox.max.x) / 2; 1198 const centerZ = (finalBox.min.z + finalBox.max.z) / 2; 1199 1200 // 计ç®åéçè·ç¦»åé«åº¦ï¼ä½¿ã头-èãèå´å®å ¨åºç°å¨è§å£å ï¼å¤´é¨å± ä¸ 1201 const padding = 0.20 * modelHeight; // 20% çç½ç¨äºè§è§èé度 1202 const visibleHeight = modelHeight + padding; 1203 const fov = camera.fov * (Math.PI / 180); 1204 // è®¡ç®æåæºè·ç¦»ï¼ä½¿æ´ä¸ªæ¨¡åï¼ä»å¤´å°èï¼å®å ¨å¯è§ 1205 // æåæº y 对å头顶ï¼z è½´éåè¶³å¤è·ç¦»ä»¥çå°å ¨èº«ï¼å æ¬èï¼ 1206 const camDist = visibleHeight / (2 * Math.tan(fov / 2)); 1207 1208 // Camera target 对å头顶ä½ç½®ï¼ä½¿å¤´å¨è§éä¸å¿ 1209 // è¿æ ·å¯ä»¥ç¡®ä¿å¤´é¨å¨ç»å¸ä¸å¤®ï¼èé¨ä¸å®å¯è§ 1210 controls.target.set(centerX, headY, centerZ); 1211 1212 // æåæºä½ç½®ï¼x, z å± ä¸ï¼y 设置为头顶é«åº¦ï¼ä¸å¤´é½å¹³ï¼ 1213 // z è½´éåè¶³å¤è·ç¦»ï¼ç¡®ä¿ä»å¤´å°èç宿´æ¨¡åé½å¯è§ 1214 camera.position.set(centerX, headY, centerZ + camDist); 1215 camera.lookAt(centerX, headY, centerZ); 1216 1217 controls.update(); 1218 1219 // 详ç»çè°è¯æ¥å¿ - è¾åºå ¨é¨åæ ä¸åæ° 1220 console.log(`ð Model bounds: ${finalSize.x.toFixed(2)} x ${finalSize.y.toFixed(2)} x ${finalSize.z.toFixed(2)}`); 1221 console.log(`ð£ Feet at Y: ${footY.toFixed(2)}, Head at Y: ${headY.toFixed(2)}`); 1222 console.log(`ð Model height: ${modelHeight.toFixed(2)}, Padding: ${padding.toFixed(2)}`); 1223 console.log(`ð Visible height: ${visibleHeight.toFixed(2)}`); 1224 console.log(`ð· Camera FOV: ${camera.fov}°, FOV (radians): ${fov.toFixed(4)}`); 1225 console.log(`ð Camera distance (z): ${camDist.toFixed(2)}`); 1226 console.log(`ð· Camera position: (${camera.position.x.toFixed(2)}, ${camera.position.y.toFixed(2)}, ${camera.position.z.toFixed(2)})`); 1227 console.log(`ð¯ Camera lookAt: (${centerX.toFixed(2)}, ${headY.toFixed(2)}, ${centerZ.toFixed(2)})`); 1228 console.log(`ð¯ Controls target: (${controls.target.x.toFixed(2)}, ${controls.target.y.toFixed(2)}, ${controls.target.z.toFixed(2)})`); 1229 console.log(`ð Center position: X=${centerX.toFixed(2)}, Z=${centerZ.toFixed(2)}`); 1230 1231 return object; 1232 } 1233 1234 // å¨ç»èªå¨äº¤æ¿ 1235 function startAnimationLoop() { 1236 const WALK_DURATION = 5000; 1237 const RUN_DURATION = 4000; 1238 1239 async function loop() { 1240 while (true) { 1241 if (currentAnimation !== 'Walking' && actions['Walking']) { 1242 actions[currentAnimation]?.fadeOut(0.5); 1243 actions['Walking'].reset().fadeIn(0.5).play(); 1244 currentAnimation = 'Walking'; 1245 } 1246 await new Promise(r => setTimeout(r, WALK_DURATION)); 1247 1248 if (currentAnimation !== 'Running' && actions['Running']) { 1249 actions[currentAnimation]?.fadeOut(0.5); 1250 actions['Running'].reset().fadeIn(0.5).play(); 1251 currentAnimation = 'Running'; 1252 } 1253 await new Promise(r => setTimeout(r, RUN_DURATION)); 1254 } 1255 } 1256 loop(); 1257 } 1258 1259 // çªå£è°æ´ 1260 function handleResize() { 1261 const newHeight = getCanvasHeight(); 1262 if (Math.abs(newHeight - currentHeight) > 5) { 1263 currentHeight = newHeight; 1264 canvas.style.height = currentHeight + 'px'; 1265 camera.aspect = CONFIG.width / currentHeight; 1266 camera.updateProjectionMatrix(); 1267 renderer.setSize(CONFIG.width, currentHeight); 1268 } 1269 } 1270 window.addEventListener('resize', handleResize); 1271 1272 // é¼ æ è·éæè½¬äºä»¶ 1273 document.addEventListener('mousemove', (event) => { 1274 // é¼ æ x å½ä¸åå° [-1, 1] 1275 const mouseX = (event.clientX / window.innerWidth) * 2 - 1; 1276 // ååè·éï¼é¼ æ å¾å·¦(mouseX < 0)ï¼æ¨¡åå¾å·¦(rotation.y < 0)ï¼é¼ æ å¾å³(mouseX > 0)ï¼æ¨¡åå¾å³(rotation.y > 0) 1277 targetRotationY = mouseX * MAX_ROTATION_RADIANS; 1278 }); 1279
1280 async function init() { 1281 try { 1282 console.log(`[AI Companion] Version: ${AI_COMPANION_VERSION}`); 1283 console.log('ð å¼å§å è½½ FBX 模å...'); 1284 1285 const [walkingFbx, runningFbx] = await Promise.all([ 1286 loadModel('./models/Walking.fbx'), 1287 loadModel('./models/Standard_Run.fbx') 1288 ]); 1289 1290 // å建å¨ç»æ··åå¨å¹¶è®¾ç½®å°åå§å¸§ï¼ç¡®ä¿å å´ç计ç®åç¡®ï¼ 1291 mixer = new THREE.AnimationMixer(walkingFbx); 1292 1293 if (walkingFbx.animations?.length) { 1294 actions['Walking'] = mixer.clipAction(walkingFbx.animations[0]); 1295 actions['Walking'].setLoop(THREE.LoopRepeat); 1296 // 设置å°åå§å¸§ä½ä¸ææ¾ (reset() å·²ç»å°å¨ç»è®¾ä¸ºåå§ç¶æ) 1297 actions['Walking'].reset(); 1298 console.log(`ð¬ Walking animation: ${walkingFbx.animations.length} clips`); 1299 } 1300 1301 if (runningFbx.animations?.length) { 1302 actions['Running'] = mixer.clipAction(runningFbx.animations[0]); 1303 actions['Running'].setLoop(THREE.LoopRepeat); 1304 console.log(`ð¬ Running animation: ${runningFbx.animations.length} clips`); 1305 } 1306 1307 // å¨å¨ç»æåç¶æä¸è®¾ç½®æ¨¡åä½ç½® 1308 model = setupModel(walkingFbx); 1309 scene.add(model); 1310 1311 // ç°å¨å¯ä»¥å¼å§ææ¾å¨ç» 1312 if (actions['Walking']) { 1313 actions['Walking'].play(); 1314 startAnimationLoop(); 1315 } 1316 1317 // 渲æå¾ªç¯ 1318 (function animate() { 1319 requestAnimationFrame(animate); 1320 const delta = clock.getDelta(); 1321 if (mixer) mixer.update(delta); 1322 1323 // å¹³æ»æè½¬ - é¼ æ è·é 1324 if (model) { 1325 model.rotation.y += (targetRotationY - model.rotation.y) * ROTATION_SMOOTHING_FACTOR; 1326 } 1327 1328 controls.update(); 1329 renderer.render(scene, camera); 1330 })(); 1331 1332 console.log('â AI Companion åå§å宿!'); 1333 1334 } catch (e) { 1335 console.error('â å 载模å失败:', e); 1336 } 1337 } 1338 1339 // åå§å 1340 canvas.style.height = currentHeight + 'px'; 1341 init(); 1342 })(); 1343 </script>
1343 1344</body> 1345</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.