1 Commits

Author SHA1 Message Date
hehh
532cbafd8e feat(audio): 重构音频播放功能以支持iframe隔离播放
- 将音频播放器移至独立的iframe中以提升性能和隔离性
- 实现主页面与iframe之间的postMessage通信机制
- 添加音频播放状态同步和UI更新逻辑
- 支持自动播放控制和用户交互检测
- 实现播放状态持久化存储和恢复功能
- 优化移动端音频控制体验
- 添加版权信息更新和国际化支持
2025-11-30 16:42:00 +08:00
22 changed files with 929 additions and 2605 deletions

View File

@@ -40,16 +40,15 @@
<meta property="wechat:description" content="我是一名充满热情的Java后端开发工程师专注于AI技术的探索与应用。">
<!-- 核心资源:使用 BootCDN 加速 -->
<link href="https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.min.css" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" href="css/about.css?version=20251125" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link href="https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/about.css?version=20251125">
<!-- Artalk 评论样式 -->
<link rel="preload" href="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" href="css/artalk.css?version=20251125" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.css">
<link rel="stylesheet" href="css/artalk.css?version=20251125">
<link rel="icon" href="favicon.ico">
<link rel="apple-touch-icon" href="./images/logo.png">
<!--IE淘汰计划-->
<script>
if (/*@cc_on!@*/false || (!!window.MSInputMethodContext && !!document.documentMode)) window.location.href = "https://imsyy.top/upgrade-your-browser/index.html?referrer=" + encodeURIComponent(window.location.href);
@@ -74,7 +73,7 @@
</a>
<div class="nav-menu">
<a href="index.html" class="nav-item" aria-label="Home">
<a href="index.html" class="nav-item">
<i class="ri-home-smile-2-line"></i>
<span class="nav-label" data-i18n="nav.home">首页</span>
</a>
@@ -82,7 +81,7 @@
<i class="ri-user-3-line"></i>
<span class="nav-label" data-i18n="nav.about">关于</span>
</a>
<a href="https://blog.hehouhui.cn" class="nav-item" aria-label="Honesty Blog">
<a href="https://blog.hehouhui.cn" class="nav-item">
<i class="ri-quill-pen-line"></i>
<span class="nav-label" data-i18n="nav.blog">博客</span>
</a>
@@ -111,7 +110,7 @@
<div class="bento-card area-profile">
<div class="profile-content">
<div class="avatar-ring">
<img src="images/avatar.jpeg" alt="Honesty" class="avatar-img" loading="lazy" width="120" height="120">
<img src="images/avatar.jpeg" alt="Honesty" class="avatar-img" loading="lazy">
<div class="status-dot" data-i18n="status.online">Online</div>
</div>
<div class="profile-info">
@@ -130,15 +129,6 @@
<a href="javascript:void(0);" onclick="toggleWechat()" class="s-icon" aria-label="WeChat QR code" tabindex="0"><i class="ri-wechat-fill"></i></a>
<a href="https://juejin.cn/user/3659591622878503" target="_blank" class="s-icon" aria-label="Juejin profile" tabindex="0"><i class="ri-code-box-line"></i></a>
</div>
<!-- 推荐分享模块 TRAE 链接https://www.trae.ai/s/8HSXCa -->
<div class="recommend-share-module">
<a href="https://www.trae.ai/s/8HSXCa" target="_blank" class="share-link" aria-label="推荐分享">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18" class="share-icon">
<path stroke="#32F08C" stroke-width="1.35" d="M3.75 9H3V6h12v3h-.75M3.75 9v6h10.5V9M3.75 9h10.5M9 6V4.75M9 6H7.75a2.5 2.5 0 0 1-2.5-2.5c0-.69.56-1.25 1.25-1.25A2.5 2.5 0 0 1 9 4.75M9 6h1.25a2.5 2.5 0 0 0 2.5-2.5c0-.69-.56-1.25-1.25-1.25A2.5 2.5 0 0 0 9 4.75M9 6v9"></path>
</svg>
<span class="badge-dot"></span>
</a>
</div>
</div>
</div>
@@ -185,7 +175,7 @@
<span class="mbti-code gradient-text">INFJ</span>
<span class="mbti-name" data-i18n="mbti.name">Advocate</span>
<span class="mbti-icon">
<img src="images/INFJ.webp" alt="INFJ" style="width:32px;height:32px;border-radius:50%;border:2px solid rgba(255,255,255,0.4)" loading="lazy" width="32" height="32"/>
<img src="images/INFJ.webp" alt="INFJ" style="width:32px;height:32px;border-radius:50%;border:2px solid rgba(255,255,255,0.4)" loading="lazy"/>
</span>
</div>
<p class="mbti-desc" data-i18n="mbti.desc">"理想主义与道德感,果断决绝的行动力。深度洞察与创意,关怀与同理心。"</p>
@@ -311,7 +301,8 @@
<button id="fab-music" class="fab-item" tabindex="0"><i class="ri-music-2-line"></i><span class="fab-text">Play</span></button>
</div>
</div>
<audio id="site-audio" class="site-audio" src="data/至少做一件离谱的事-Kiri T.mp3" autoplay loop preload="auto"></audio>
<!-- 隐藏的音频播放iframe -->
<iframe id="audio-player-iframe" src="audio-player.html" style="display: none;"></iframe>
</main>
<!-- 微信弹窗 -->
@@ -320,7 +311,8 @@
<button class="modal-close" onclick="toggleWechat()"><i class="ri-close-line"></i></button>
<h3 data-i18n="modal.wechat">Official Account</h3>
<div class="qr-box">
<img src="./images/mp-honesy.jpg" alt="WeChat QR" onerror="this.style.display='none';this.nextElementSibling.style.display='block'" loading="lazy" width="200" height="200">
<img src="./images/mp-honesy.jpg" alt="WeChat QR" onerror="this.style.display='none';this.nextElementSibling.style.display='block'" loading="lazy">
<!-- <div class="qr-fallback">QR Load Failed</div>-->
</div>
<p data-i18n="modal.desc">Scan to follow Tech Share</p>
</div>
@@ -335,25 +327,24 @@
</div>
<!-- 脚本BootCDN jQuery / Artalk -->
<script src="js/jquery.min.js" defer></script>
<script src="js/config.js?version=20251125" defer></script>
<script src="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.js" defer></script>
<script src="js/about.js?version=20251125" defer></script>
<script defer src="https://events.vercount.one/js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/config.js?version=20251125"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.js"></script>
<script src="js/about.js?version=20251125"></script>
<!-- 不蒜子统计 -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// 动态加载不蒜子统计脚本
const script = document.createElement('script');
script.src = "//cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.abbr.min.js";
script.src = SiteConfig.analytics.busuanzi.src;
script.async = true;
document.head.appendChild(script);
});
</script>
<script>
try {
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"3OBGjwDdEIRS7XZ1",ck:"3OBGjwDdEIRS7XZ1"});
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id: SiteConfig.analytics.tencent.id, ck: SiteConfig.analytics.tencent.ck});
} catch (e) {
console.log("51.la统计错误", e);
}
@@ -365,7 +356,7 @@
(function () {
try {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?ae2a009a75b13c21d5121ee51375ea4e";
hm.src = SiteConfig.analytics.baidu.src;
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
} catch (e) {
@@ -379,8 +370,8 @@
<script>
// 监听不蒜子数据的错误兜底
function initFormatter() {
const pvEl = document.getElementById("busuanzi_site_pv");
const uvEl = document.getElementById("busuanzi_site_uv");
const pvEl = document.getElementById(SiteConfig.analytics.busuanzi.site_pv_id);
const uvEl = document.getElementById(SiteConfig.analytics.busuanzi.site_uv_id);
if (!pvEl && !uvEl) return;
console.log('[Busuanzi]', 'Formatting... Listener observer');
@@ -410,12 +401,5 @@
initFormatter();
}
</script>
<noscript>
<link href="https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/remixicon/3.5.0/remixicon.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/about.css?version=20251125">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/artalk/2.9.1/Artalk.css">
<link rel="stylesheet" href="css/artalk.css?version=20251125">
</noscript>
</body>
</html>

208
audio-player.html Normal file
View File

@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Player</title>
<style>
body {
margin: 0;
padding: 0;
background: transparent;
}
</style>
</head>
<body>
<audio id="audio-player" controls autoplay style="display: none;"></audio>
<script>
const audio = document.getElementById('audio-player');
let currentSrc = '';
let isStateBroadcasting = false;
// 监听来自主页面的消息
window.addEventListener('message', function(event) {
// 允许来自同域的所有窗口的消息
if (event.origin !== window.location.origin) return;
const data = event.data;
switch (data.action) {
case 'play':
// 如果当前正在播放,则暂停;否则播放
if (audio.paused) {
audio.play().catch(e => console.error('播放失败:', e));
} else {
audio.pause();
}
// 发送状态更新回所有可能的窗口
broadcastState();
break;
case 'pause':
if (!audio.paused) {
audio.pause();
}
broadcastState();
break;
case 'setTrack':
if (currentSrc !== data.src) {
currentSrc = data.src;
audio.src = data.src;
audio.load();
// 自动播放新曲目
setTimeout(() => {
audio.play().catch(e => console.error('播放失败:', e));
broadcastState();
}, 100);
}
break;
case 'setVolume':
audio.volume = data.volume;
broadcastState();
break;
case 'getCurrentState':
// 向请求方发送当前状态
event.source.postMessage({
action: 'currentState',
playing: !audio.paused,
src: audio.src,
volume: audio.volume,
currentTime: audio.currentTime,
duration: audio.duration || 0
}, event.origin);
break;
}
});
// 广播状态到所有可能监听的窗口
function broadcastState() {
if (isStateBroadcasting) return; // 防止状态广播循环
isStateBroadcasting = true;
window.parent.postMessage({
action: 'stateChange',
playing: !audio.paused,
src: audio.src,
volume: audio.volume,
currentTime: audio.currentTime,
duration: audio.duration || 0
}, '*');
// 保存播放状态
savePlaybackState();
setTimeout(() => {
isStateBroadcasting = false;
}, 10);
}
// 监听音频事件并通知主页面
audio.addEventListener('play', function() {
broadcastState();
});
audio.addEventListener('pause', function() {
broadcastState();
});
audio.addEventListener('ended', function() {
window.parent.postMessage({
action: 'trackEnded'
}, '*');
});
// 页面加载完成后通知主页面
window.addEventListener('load', function() {
// 恢复之前保存的播放状态
restorePlaybackState();
window.parent.postMessage({
action: 'playerReady'
}, '*');
});
// 页面可见性变化时处理
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
// 页面变为可见时,广播当前状态
setTimeout(broadcastState, 100);
}
});
// 页面即将卸载时保存播放状态
window.addEventListener('beforeunload', function() {
savePlaybackState();
});
// 保存播放状态到 sessionStorage
function savePlaybackState() {
try {
sessionStorage.setItem('audioState', JSON.stringify({
src: audio.src,
currentTime: audio.currentTime,
playing: !audio.paused,
volume: audio.volume,
timestamp: Date.now()
}));
} catch (e) {
console.error('保存音频状态失败:', e);
}
}
// 从 sessionStorage 恢复播放状态
function restorePlaybackState() {
try {
const savedState = sessionStorage.getItem('audioState');
if (savedState) {
const state = JSON.parse(savedState);
// 如果状态保存时间不超过1小时则恢复播放
if (Date.now() - state.timestamp < 3600000) {
currentSrc = state.src;
audio.src = state.src;
audio.volume = state.volume !== undefined ? state.volume : 1.0;
audio.currentTime = state.currentTime || 0;
// 确保在用户交互后才尝试播放
if (state.playing) {
// 检查是否已经有用户交互
if (document.hasFocus()) {
// 稍微延迟播放,确保一切准备就绪
setTimeout(() => {
audio.play().catch(e => {
console.error('恢复播放失败:', e);
// 如果自动播放失败,等待用户交互后再播放
const tryPlayOnInteraction = () => {
audio.play().catch(console.error);
document.removeEventListener('click', tryPlayOnInteraction);
document.removeEventListener('touchstart', tryPlayOnInteraction);
};
document.addEventListener('click', tryPlayOnInteraction, { once: true });
document.addEventListener('touchstart', tryPlayOnInteraction, { once: true });
});
}, 300);
} else {
// 等待用户交互后再播放
const tryPlayOnInteraction = () => {
audio.play().catch(console.error);
document.removeEventListener('click', tryPlayOnInteraction);
document.removeEventListener('touchstart', tryPlayOnInteraction);
};
document.addEventListener('click', tryPlayOnInteraction, { once: true });
document.addEventListener('touchstart', tryPlayOnInteraction, { once: true });
}
}
}
}
} catch (e) {
console.error('恢复音频状态失败:', e);
}
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -584,6 +584,8 @@ body {
contain: layout style;
}
.avatar-ring {
position: relative;
width: 120px;
@@ -636,48 +638,6 @@ body {
justify-content: center;
gap: 4px;
}
/* Social Links */
.social-dock {
display: flex;
gap: 12px;
margin-top: 20px;
}
.s-icon {
width: 46px;
height: 46px;
border-radius: 50%;
background: rgba(128, 128, 128, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.3s;
text-decoration: none;
}
.s-icon:hover {
background: var(--accent);
color: #fff;
transform: translateY(-3px);
}
.s-icon:hover i {
color: transparent !important;
-webkit-text-fill-color: transparent;
}
.s-icon i {
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.s-icon[href*="github.com"] i { background: var(--gradient-7); }
.s-icon[href^="mailto:"] i { background: var(--gradient-6); }
.s-icon[href*="blog.hehouhui.cn"] i { background: var(--gradient-3); }
.s-icon[href*="zhihu.com"] i { background: var(--gradient-4); }
.s-icon[href*="juejin.cn"] i { background: var(--gradient-1); }
.s-icon[onclick*="toggleWechat"] i { background: var(--gradient-5); }
/* Tablet Profile Layout */
@media (min-width: 769px) and (max-width: 1024px) {
.area-profile {
@@ -729,40 +689,6 @@ body {
.desktop-social {
display: none;
}
/* 移动端推荐分享模块调整 */
.recommend-share-module {
right: 15px;
bottom: 15px;
opacity: 0;
transform: translateY(10px);
animation: shareModuleFadeIn 0.5s ease-out 0.5s forwards;
}
@keyframes shareModuleFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.share-link {
width: 36px;
height: 36px;
}
.share-icon {
width: 16px;
height: 16px;
}
.badge-dot {
width: 6px;
height: 6px;
top: 1px;
right: 1px;
border-width: 1px;
}
}
/* Night Theme Profile Styles */
@@ -778,10 +704,10 @@ body {
background-clip: text !important;
color: transparent !important;
text-shadow:
0 0 2px rgba(255, 126, 179, 0.3),
0 0 6px rgba(255, 117, 140, 0.1),
0 0 32px rgba(255, 107, 107, 0.4),
0 0 10px rgba(255, 154, 139, 0.5);
0 0 10px rgba(255, 126, 179, 0.8),
0 0 20px rgba(255, 117, 140, 0.7),
0 0 30px rgba(255, 107, 107, 0.6),
0 0 40px rgba(255, 154, 139, 0.5);
transform: translateZ(0);
-webkit-text-stroke: 0.3px rgba(0, 0, 0, 0.3);
}
@@ -799,29 +725,16 @@ body {
-webkit-text-stroke: 0.2px rgba(0, 0, 0, 0.2);
}
/* 推荐分享模块 */
.recommend-share-module {
position: absolute;
right: 20px;
bottom: 20px;
opacity: 0;
transform: translateY(10px);
animation: shareModuleFadeIn 0.5s ease-out 0.5s forwards;
/* Social Links */
.social-dock {
display: flex;
gap: 12px;
margin-top: 20px;
}
@keyframes shareModuleFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.share-link {
position: relative;
width: 42px;
height: 42px;
.s-icon {
width: 46px;
height: 46px;
border-radius: 50%;
background: rgba(128, 128, 128, 0.1);
display: flex;
@@ -830,51 +743,29 @@ body {
color: var(--text-secondary);
transition: all 0.3s;
text-decoration: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.share-link:hover {
.s-icon:hover {
background: var(--accent);
color: #fff;
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.share-icon {
width: 18px;
height: 18px;
.s-icon:hover i {
color: transparent !important;
-webkit-text-fill-color: transparent;
}
.share-icon path {
stroke: #32F08C;
}
.badge-dot {
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
background: #ff4757;
border-radius: 50%;
border: 2px solid var(--bg-base);
}
/* 夜间模式下的推荐分享模块 */
[data-theme="night"] .share-link {
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
[data-theme="night"] .share-link:hover {
box-shadow: 0 4px 15px rgba(108, 92, 231, 0.4);
}
/* 夜间模式下的SVG图标 */
[data-theme="night"] .share-icon path {
stroke: #32F08C;
filter: drop-shadow(0 0 4px rgba(50, 240, 140, 0.4));
.s-icon i {
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.s-icon[href*="github.com"] i { background: var(--gradient-7); }
.s-icon[href^="mailto:"] i { background: var(--gradient-6); }
.s-icon[href*="blog.hehouhui.cn"] i { background: var(--gradient-3); }
.s-icon[href*="zhihu.com"] i { background: var(--gradient-4); }
.s-icon[href*="juejin.cn"] i { background: var(--gradient-1); }
.s-icon[onclick*="toggleWechat"] i { background: var(--gradient-5); }
/* Night Theme Social Styles */
[data-theme="night"] .s-icon {
@@ -894,19 +785,9 @@ body {
display: none !important;
}
.social-dock {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
justify-items: center;
}
.s-icon {
width: 42px;
height: 42px;
}
.s-icon i {
font-size: 1.6rem;
}
.social-dock { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; justify-items: center; }
.s-icon { width: 42px; height: 42px; }
.s-icon i { font-size: 1.6rem; }
}
/* Mobile Social Layout */
@@ -920,8 +801,9 @@ body {
justify-content: space-around;
padding: 20px;
}
.desktop-social {
display: none;
}
.ms-btn {
width: 40px;
height: 40px;
@@ -1123,18 +1005,6 @@ body {
text-shadow: 0 0 5px rgba(255, 255, 255, 0.1);
}
@media (min-width: 769px) and (max-width: 1445px){
[data-lang="en"] .stat-key {
font-size: 0.5rem !important;
}
}
@media (min-width: 1030px) and (max-width: 1445px) {
[data-lang="en"] .mbti-tags .tag {
font-size: 0.5rem !important;
}
}
/* Responsive Stats Layout */
@media (min-width: 1025px) and (max-width: 1445px), (min-width: 1201px) {
.area-stats {
@@ -1525,8 +1395,8 @@ body {
}
[data-theme="day"] .tech-tag-3d.tag-color-23,
[data-theme="day"] .tech-tag-mobile.tag-color-23 {
color: #d57eeb !important;
-webkit-text-fill-color: #d57eeb !important;
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
[data-theme="day"] .tech-tag-3d.tag-color-24,
[data-theme="day"] .tech-tag-mobile.tag-color-24 {
@@ -2425,7 +2295,12 @@ html, body {
overflow-x: hidden;
}
/* Fix Stat Key Font Size on English Mobile */
@media (max-width: 768px) {
[data-lang="en"] .stat-key {
font-size: 6px !important;
}
}
/* Final Theme Consistency Checks */
[data-theme="night"] {

View File

@@ -6,7 +6,15 @@
contain: layout style;
}
/* 确保评论区域适配白天/黑夜模式 */
#artalk-container .atk-main-editor {
background: var(--glass-bg);
border: var(--glass-border);
box-shadow: var(--glass-shadow);
backdrop-filter: blur(var(--glass-blur)) saturate(130%);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(130%);
border-radius: 24px;
}
#artalk-container .atk-comment-wrap {
background: rgba(128, 128, 128, 0.03);
@@ -442,12 +450,6 @@
height: 28px !important;
object-fit: cover; /* 修复移动端头像拉伸问题 */
}
.atk-comment>.atk-avatar img {
object-fit: cover;
object-position: center;
width: 28px !important;
height: 28px !important;
}
.atk-meta {
font-size: 12px !important;

Binary file not shown.

View File

@@ -15,18 +15,11 @@
},
{
"name": "Home",
"stargazers_count": 3,
"stargazers_count": 2,
"forks_count": 0,
"description": "现代化个人主页,融合科技感与个性化元素,展示个人技术栈、开源项目和博客文章。",
"html_url": "https://github.com/listener-He/Home"
},
{
"name": "auto-ip2region",
"stargazers_count": 2,
"forks_count": 0,
"description": "Auto IP2Region 是一个智能化的IP地址地理信息解析库它结合了本地数据库和多个免费在线API服务通过智能负载均衡和自动故障转移机制为您提供准确、可靠的IP地理位置信息查询服务。",
"html_url": "https://github.com/listener-He/auto-ip2region"
},
{
"name": "collection-complete",
"stargazers_count": 2,
@@ -41,13 +34,6 @@
"description": "",
"html_url": "https://github.com/listener-He/keycloak-services-social-weixin"
},
{
"name": "fast-blur",
"stargazers_count": 1,
"forks_count": 0,
"description": "High-performance, lightweight data obfuscation library for Java. Offers fast, reversible data transformation without traditional encryption overhead. Ideal for caching, logging, and performance-critical applications. / 专为Java设计的高性能轻量级数据混淆库。提供快速、可逆的数据转换无需传统加密开销。适用于缓存、日志和性能关键型应用。",
"html_url": "https://github.com/listener-He/fast-blur"
},
{
"name": "Notion-Wechat-Blog",
"stargazers_count": 1,

View File

@@ -10,7 +10,7 @@
"blog": "https://www.hehouhui.cn",
"hireable": true,
"bio": "Hi, Im Honesty—Shanghai Java/AI Dev (7+ yrs). Spring AI, LLM, TensorFlow, Faiss. Write tech content, cycle for inspiration. AI-obsessed.",
"public_repos": 167,
"public_repos": 165,
"public_gists": 0,
"followers": 6,
"following": 12,

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
{
"rewrites": [
{
"source": "/.well-known/*.txt",
"destination": "/well-known/:splat.txt"
}
]
}

View File

@@ -335,7 +335,10 @@
<script type="text/javascript" src="js/fetch.min.js"></script>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript" src="js/main.js?version=3"></script>
<script defer src="https://events.vercount.one/js"></script>
<!--<script type="text/javascript" src="js/bj.js"></script>-->
<!--<script type="text/javascript" src="https://cdn.jsdmirror.com/gh/listener-He/Home/js/moments.js"></script>-->
<script async src="https://analyse.hehouhui.cn/tracker.js" data-ackee-server="https://analyse.hehouhui.cn"
data-ackee-domain-id="7887135f-a413-46e2-a98c-52d4f18d9973"></script>
<!-- 不蒜子统计 -->
<script>
@@ -410,5 +413,26 @@
}()
}({id: SiteConfig.analytics.tencent.id, ck: SiteConfig.analytics.tencent.ck});
</script>
<!-- 隐藏的音频播放iframe -->
<iframe id="audio-player-iframe" src="audio-player.html" style="display: none;"></iframe>
<script>
// 音频控制逻辑
let audioIframe = document.getElementById('audio-player-iframe');
// 监听来自iframe的消息
window.addEventListener('message', function(event) {
// 确保消息来自我们的iframe
if (event.source !== audioIframe.contentWindow) return;
const data = event.data;
if (data.playing && data.action === 'playerReady') {
// iframe准备就绪设置初始音频
audioIframe.contentWindow.postMessage({
action: 'setTrack',
src: 'data/至少做一件离谱的事-Kiri T_compressed.mp3'
}, '*');
}
});
</script>
</body>
</html>

215
js/StarrySky.js Normal file
View File

@@ -0,0 +1,215 @@
/**
* Starry Sky
*
* 作者: DoWake
* 描述使用Canvas绘制星空
* 地址https://github.com/DoWake/StarrySky
* 日期2023/03/02
*/
const StarrySky = function () {
//Canvas元素
let canvasElement;
//Canvas 2D对象
let canvasContext;
//Canvas 宽度
let canvasWidth;
//Canvas 高度
let canvasHeight;
//星星列表
let starList;
//星星颜色列表rgb格式"255, 255, 255"
let starColorList;
//星星半径大小
let starRadius;
//焦距等级与canvasWidth相乘必须大于0
let focalDistanceLevel;
//星星数量等级与canvasWidth相乘必须大于0
let starCountLevel;
//星星速度等级与焦距相乘必须大于0
let starSpeedLevel;
//焦距
let focalDistance;
//星星数量
let starCount;
//执行动画
let rAF;
return {
//初始化
init: function (canvas_element) {
if (canvas_element && canvas_element.nodeName === "CANVAS") {
canvasElement = canvas_element;
canvasElement.width = canvasElement.clientWidth;
canvasElement.height = canvasElement.clientHeight;
canvasElement.style.backgroundColor = "black";
canvasContext = canvasElement.getContext("2d");
canvasWidth = canvasElement.clientWidth;
canvasHeight = canvasElement.clientHeight;
starColorList = ["255, 255, 255"];
starRadius = 1;
focalDistanceLevel = 0.4;
starCountLevel = 0.2;
starSpeedLevel = 0.0005;
focalDistance = canvasWidth * focalDistanceLevel;
starCount = Math.ceil(canvasWidth * starCountLevel);
starList = [];
for (let i = 0; i < starCount; i++) {
starList[i] = {
x: canvasWidth * (0.1 + 0.8 * Math.random()),
y: canvasHeight * (0.1 + 0.8 * Math.random()),
z: focalDistance * Math.random(),
color: starColorList[Math.ceil(Math.random() * 1000) % starColorList.length]
}
}
const self = this;
window.addEventListener("resize", self.throttle(function () {
canvasElement.width = canvasElement.clientWidth;
canvasElement.height = canvasElement.clientHeight;
canvasWidth = canvasElement.clientWidth;
canvasHeight = canvasElement.clientHeight;
focalDistance = canvasWidth * focalDistanceLevel;
const starCount2 = Math.ceil(canvasWidth * starCountLevel);
if (starCount > starCount2) {
starList.splice(starCount2);
} else {
let num = starCount2 - starCount;
while (num--) {
starList.push({
x: canvasWidth * (0.1 + 0.8 * Math.random()),
y: canvasHeight * (0.1 + 0.8 * Math.random()),
z: focalDistance * Math.random(),
color: starColorList[Math.ceil(Math.random() * 1000) % starColorList.length]
});
}
}
starCount = Math.ceil(canvasWidth * starCountLevel);
}, 200), { passive: true });
} else {
console.error('初始化失败必须传入Canvas元素');
}
},
//设置星空背景颜色
setSkyColor: function (sky_color = "black") {
canvasElement.style.backgroundColor = sky_color;
},
//设置星星半径大小
setStarRadius: function (star_radius = 1) {
starRadius = star_radius;
},
//设置焦距等级
setFocalDistanceLevel: function (focal_distance_level = 0.4) {
focalDistanceLevel = focal_distance_level;
focalDistance = canvasWidth * focalDistanceLevel
},
//设置星星数量等级
setStarCountLevel: function (star_count_level = 0.2) {
starCountLevel = star_count_level;
const starCount2 = Math.ceil(canvasWidth * starCountLevel);
if (starCount > starCount2) {
starList.splice(starCount2);
} else {
let num = starCount2 - starCount;
while (num--) {
starList.push({
x: canvasWidth * (0.1 + 0.8 * Math.random()),
y: canvasHeight * (0.1 + 0.8 * Math.random()),
z: focalDistance * Math.random(),
color: starColorList[Math.ceil(Math.random() * 1000) % starColorList.length]
});
}
}
starCount = Math.ceil(canvasWidth * starCountLevel);
},
//设置星星速度等级
setStarSpeedLevel: function (star_speed_level = 0.0005) {
starSpeedLevel = star_speed_level
},
/**
* 设置星星颜色
* @param {Array|String} color 星星颜色
* @param {Boolean} mode 是否立刻同步颜色
*/
setStarColorList: function (color, mode = false) {
if (typeof color === 'object') {
starColorList = color;
} else if (typeof color === 'string') {
starColorList.push(color);
}
if (mode) {
for (let i = 0; i < starList.length; i++) {
starList[i]["color"] = starColorList[Math.ceil(Math.random() * 1000) % starColorList.length];
}
}
},
//渲染
render: function () {
const starSpeed = canvasWidth * focalDistanceLevel * starSpeedLevel;
//清空画布
canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
//计算位置
for (let i = 0; i < starList.length; i++) {
const star = starList[i];
const star_x = (star["x"] - canvasWidth / 2) * (focalDistance / star["z"]) + canvasWidth / 2;
const star_y = (star["y"] - canvasHeight / 2) * (focalDistance / star["z"]) + canvasHeight / 2;
star["z"] -= starSpeed;
if (star["z"] > 0 && star["z"] <= focalDistance && star_x >= -20 && star_x <= canvasWidth + 20 && star_y >= -20 && star_y <= canvasHeight + 20) {
const star_radius = starRadius * (focalDistance / star["z"] * 0.8);
const star_opacity = 1 - 0.8 * (star["z"] / focalDistance);
canvasContext.fillStyle = "rgba(" + star["color"] + ", " + star_opacity + ")";
canvasContext.shadowOffsetX = 0;
canvasContext.shadowOffsetY = 0;
canvasContext.shadowColor = "rgb(" + star["color"] + ")";
canvasContext.shadowBlur = 5;
canvasContext.beginPath();
canvasContext.arc(star_x, star_y, star_radius, 0, 2 * Math.PI);
canvasContext.fill();
} else {
const z = focalDistance * Math.random();
star["x"] = canvasWidth * (0.1 + 0.8 * Math.random());
star["y"] = canvasHeight * (0.1 + 0.8 * Math.random());
star["z"] = z;
star["color"] = starColorList[Math.ceil(Math.random() * 1000) % starColorList.length];
}
}
const self = this;
rAF = window.requestAnimationFrame(function () {
self.render();
});
},
//销毁
destroy: function () {
window.cancelAnimationFrame(rAF);
starList = [];
canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
canvasElement.width = 0;
canvasElement.height = 0;
},
//防抖
debounce: function (func, time = 200) {
let timeId;
return function () {
if (timeId) {
clearTimeout(timeId);
}
timeId = setTimeout(function () {
func();
}, time);
}
},
//节流
throttle: function (func, time = 200) {
let timeId = null;
let pre = 0;
return function () {
if (Date.now() - pre > time) {
clearTimeout(timeId);
pre = Date.now();
func();
} else {
timeId = setTimeout(func, time);
}
};
}
}
}();

View File

@@ -13,7 +13,7 @@ function getStoredLanguage() {
// 公共方法:设置本地存储的主题设置
function setStoredTheme(theme) {
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme';
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
localStorage.setItem(cacheKey, JSON.stringify({
value: theme, time: new Date().getTime()
}));
@@ -534,6 +534,9 @@ class DataManager {
=========================== */
class UIManager {
constructor() {
this.userInteracted = false;
this.audioPlayer = null;
this.audioIframe = null;
this.initTechCloud();
this.initModal();
this.initArtalk();
@@ -1080,10 +1083,33 @@ class UIManager {
const theme = getStoredTheme();
fLang.querySelector('.fab-text').textContent = lang === 'zh' ? 'English' : '中文';
fTheme.querySelector('.fab-text').textContent = theme === 'night' ? 'Day' : 'Night';
const playing = (this.audio && !this.audio.paused);
fMusic.querySelector('.fab-text').textContent = lang === 'zh' ? (playing ? '暂停' : '播放') : (playing ? 'Pause' : 'Play');
// 音频播放状态需要通过iframe通信获取
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
// 请求当前播放状态
audioIframe.contentWindow.postMessage({
action: 'getCurrentState'
}, '*');
}
});
};
// 监听来自iframe的音频状态更新
window.addEventListener('message', (event) => {
const audioIframe = document.getElementById('audio-player-iframe');
if (!audioIframe || event.source !== audioIframe.contentWindow) return;
const data = event.data;
if (data.action === 'currentState') {
const fMusic = document.getElementById('fab-music');
if (fMusic) {
const lang = getStoredLanguage();
fMusic.querySelector('.fab-text').textContent =
lang === 'zh' ? (data.playing ? '暂停' : '播放') : (data.playing ? 'Pause' : 'Play');
}
}
});
main.addEventListener('click', () => {
menu.classList.toggle('open');
main.setAttribute('aria-expanded', menu.classList.contains('open') ? 'true' : 'false');
@@ -1101,17 +1127,15 @@ class UIManager {
requestAnimationFrame(updateLabels);
});
fMusic.addEventListener('click', () => {
if (this.audio) {
if (this.audio.paused) {
this.audio.play().catch(() => {
});
// 清除暂停时间记录,允许下次自动播放
this.clearMusicPauseTime();
} else {
this.audio.pause();
// 记录暂停时间
this.setMusicPauseTime();
}
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
// 直接发送播放指令让iframe内部处理播放/暂停切换
audioIframe.contentWindow.postMessage({
action: 'play'
}, '*');
// 记录用户操作
this.setMusicPauseTime(); // 记录暂停时间
}
// 延迟更新标签以避免阻塞
requestAnimationFrame(updateLabels);
@@ -1122,33 +1146,33 @@ class UIManager {
initAudio() {
const el = document.getElementById('site-audio');
if (!el) return;
this.audio = el;
this.audio.loop = true;
// 获取已存在的iframe或创建新的
let audioIframe = document.getElementById('audio-player-iframe');
if (!audioIframe) {
audioIframe = document.createElement('iframe');
audioIframe.src = 'audio-player.html';
audioIframe.style.display = 'none';
audioIframe.id = 'audio-player-iframe';
document.body.appendChild(audioIframe);
}
this.audioIframe = audioIframe;
// 页面加载完成后根据条件决定是否播放
window.addEventListener('load', () => {
const autoPlayer = () => {
// 检查是否在24小时内用户暂停过音乐
const shouldRemainPaused = this.shouldMusicRemainPaused();
// 如果不应该保持暂停状态,则尝试播放
if (!shouldRemainPaused) {
let userInteracted = true;
this.audio.play().catch(() => {
// 静默处理播放失败
userInteracted = false;
});
// 添加用户交互检查,避免浏览器阻止自动播放
const attemptAutoplay = () => {
// 检查是否已有用户交互
if (this.userInteracted === false) {
if (this.userInteracted) {
this.playAudio();
} else {
// 添加一次性用户交互监听器
const enableAudio = () => {
this.userInteracted = true;
setTimeout(() => {
this.audio.play().catch(() => {
});
}, 1000);
this.playAudio();
document.removeEventListener('click', enableAudio);
document.removeEventListener('touchstart', enableAudio);
document.removeEventListener('keydown', enableAudio);
@@ -1161,9 +1185,86 @@ class UIManager {
document.addEventListener('mousemove', enableAudio, { once: true });
}
};
requestAnimationFrame(attemptAutoplay);
setTimeout(attemptAutoplay, 500); // 稍微延迟以确保iframe加载完成
}
}
// 监听iframe发来的消息
const handleMessage = (event) => {
// 确保消息来自我们的iframe
if (event.source !== this.audioIframe.contentWindow) return;
const data = event.data;
switch (data.action) {
case 'playerReady':
// iframe准备就绪设置初始音频
this.setAudioTrack('data/至少做一件离谱的事-Kiri T_compressed.mp3');
autoPlayer();
break;
case 'stateChange':
// 音频状态改变更新UI
this.updateAudioUI(data.playing);
break;
case 'trackEnded':
// 曲目结束
this.updateAudioUI(false);
break;
case 'currentState':
// 当前状态响应
this.updateAudioUI(data.playing);
if (!data.playing) {
autoPlayer();
}
break;
}
};
window.addEventListener('message', handleMessage);
}
// 播放音频
playAudio() {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
audioIframe.contentWindow.postMessage({
action: 'play'
}, '*');
}
}
// 暂停音频
pauseAudio() {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
audioIframe.contentWindow.postMessage({
action: 'pause'
}, '*');
}
}
// 设置音频曲目
setAudioTrack(src) {
const audioIframe = document.getElementById('audio-player-iframe');
if (audioIframe && audioIframe.contentWindow) {
audioIframe.contentWindow.postMessage({
action: 'setTrack',
src: src
}, '*');
}
}
// 更新音频UI状态
updateAudioUI(playing) {
// 更新移动端fab按钮的文本
const fMusic = document.getElementById('fab-music');
if (fMusic) {
const lang = getStoredLanguage();
fMusic.querySelector('.fab-text').textContent =
lang === 'zh' ? (playing ? '暂停' : '播放') : (playing ? 'Pause' : 'Play');
}
});
}
updateCustomStyles(container, theme) {

View File

@@ -0,0 +1,42 @@
// audio-service-worker.js
// Service Worker for background audio playback
self.addEventListener('install', event => {
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
});
// 监听来自主页面的消息
self.addEventListener('message', async event => {
const client = event.source;
const data = event.data;
switch (data.action) {
case 'play':
// 这里只是示例实际上Service Worker无法直接播放音频
// 我们需要采用另一种方式实现
break;
case 'pause':
break;
case 'setTrack':
break;
}
});
// 使用 Broadcast Channel API 在页面间通信
const broadcastChannel = new BroadcastChannel('audio-control');
broadcastChannel.addEventListener('message', event => {
const data = event.data;
// 将消息转发给所有客户端
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage(data);
});
});
});

View File

@@ -1,7 +1,6 @@
// 配置文件 - 提取自各个JavaScript文件的关键配置
// 创建日期: 2025-11-20
(function() {
const SiteConfig = {
// bj.js 配置
stars: {
@@ -44,9 +43,9 @@
// 通用缓存键与TTL毫秒
cacheKeys: {
github: { key: 'gh_data', ttlMs: 36000000 },
blog: { key: 'blog_data', ttlMs: 3600000 },
theme: { key: 'theme', ttlMs: 3600000 }
github: { key: 'gh_data_v2', ttlMs: 36000000 },
blog: { key: 'blog_data_v2', ttlMs: 3600000 },
theme: { key: 'theme_v2', ttlMs: 3600000 }
},
techStack: [
@@ -178,4 +177,3 @@
} else if (typeof window !== 'undefined') {
window.SiteConfig = SiteConfig;
}
})();

58
js/moments.js Normal file
View File

@@ -0,0 +1,58 @@
$(document).ready(function () {
const iframe = document.getElementById('moment-frame');
const momentsContainer = document.getElementById('moments-container');
function animateIframe() {
momentsContainer.style.display = 'block';
momentsContainer.classList.add('visible');
}
function openMoments(url) {
if (iframe.src == null || iframe.src === '' || iframe.src !== url) {
iframe.src = url;
iframe.onload = () => {
setTimeout(() => {
animateIframe();
}, 300); // 延迟更自然
};
} else {
animateIframe();
}
}
// 处理瞬间链接点击事件
$('.moments-link').on('click', function (e) {
e.preventDefault(); // 阻止默认跳转
// 获取链接地址
const url = "https://moments.hehouhui.cn";
// 判断是否是移动端
const isMobile = /iPhone|Android/i.test(navigator.userAgent);
if (isMobile) {
// 移动端:直接跳转
window.location.href = url;
} else {
// PC端在iframe中显示
openMoments(url);
}
});
// 关闭按钮点击事件
$('.close-btn').on('click', function () {
momentsContainer.classList.remove('visible');
setTimeout(() => {
momentsContainer.style.display = 'none';
}, 500); // 等待动画结束后隐藏
});
// 遮罩层点击事件 点击空白处关闭模拟器
$('.overlay').on('click', function () {
momentsContainer.classList.remove('visible');
setTimeout(() => {
momentsContainer.style.display = 'none';
}, 500); // 等待动画结束后隐藏
});
});

620
me.html
View File

@@ -1,620 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>ETHEREAL | HAND PHYSICS</title>
<style>
:root {
--bg-color: #020202;
--ui-font: 'Helvetica Neue', 'Arial', sans-serif;
--serif-font: 'Times New Roman', serif;
}
body {
margin: 0;
overflow: hidden;
background-color: var(--bg-color);
font-family: var(--ui-font);
cursor: none; /* 隐藏鼠标 */
}
#canvas-container {
position: fixed;
inset: 0;
z-index: 1;
}
#input-video { display: none; }
/* 极简主义 UI */
#ui-layer {
position: fixed;
inset: 0;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px;
mix-blend-mode: exclusion; /* 高级混合模式 */
}
.top-bar {
display: flex;
justify-content: space-between;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 2px;
color: #fff;
opacity: 0.7;
}
.center-stage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 100%;
}
.art-title {
font-family: var(--serif-font);
font-size: 48px;
font-weight: 100;
letter-spacing: 12px;
color: #fff;
opacity: 0;
transition: opacity 2s ease;
}
.art-sub {
font-size: 10px;
letter-spacing: 6px;
color: #fff;
margin-top: 15px;
opacity: 0;
transition: opacity 2s 0.5s ease;
}
.visible { opacity: 1 !important; }
.debug-info {
position: absolute;
bottom: 40px;
left: 40px;
font-family: monospace;
font-size: 10px;
color: rgba(255,255,255,0.4);
line-height: 1.5;
}
/* 加载器 */
#loader {
position: fixed;
inset: 0;
background: #000;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 1s;
}
.loader-line {
width: 0%;
height: 1px;
background: #fff;
transition: width 0.5s;
}
</style>
<!-- 核心库 -->
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
<!-- AI 视觉库 -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
</head>
<body>
<div id="loader"><div class="loader-line" id="loader-bar"></div></div>
<div id="canvas-container"></div>
<video id="input-video" playsinline></video>
<div id="ui-layer">
<div class="top-bar">
<span id="fps-display">FPS: --</span>
<span id="theme-display">CALIBRATING REALITY...</span>
</div>
<div class="center-stage">
<div class="art-title" id="title">VOID</div>
<div class="art-sub" id="subtitle">INTERACTIVE FLUID DYNAMICS</div>
</div>
<div class="debug-info" id="hand-state">
HANDS: SEARCHING<br>
FORCE: 0.00
</div>
</div>
<script>
/**
* ============================================================================
* 1. 数学工具与配置 (Math & Config)
* ============================================================================
*/
const CONFIG = {
particleCount: 8000, // 粒子数量,适中以保证物理计算性能
particleSize: 1.8, // 基础大小
bloomStrength: 1.2, // 辉光强度
handForceRadius: 15.0, // 手掌影响半径
friction: 0.96, // 物理摩擦力 (越小停得越快)
returnSpeed: 0.008, // 回归原位的速度 (弹性)
noiseScale: 0.02, // 噪声纹理缩放
curlStrength: 0.5 // 旋度强度 (流体感)
};
const Simplex = new SimplexNoise();
// 颜色主题定义 (Palette)
const THEMES = {
DEFAULT: { name: 'VOID / 虚空', colors: [0x888888, 0xffffff, 0x444444] },
SPRING: { name: 'FLORA / 生机', colors: [0xff3366, 0xffdd00, 0x00ff88] },
SUMMER: { name: 'OCEAN / 碧海', colors: [0x00ffff, 0x0066ff, 0xffffff] },
AUTUMN: { name: 'EMBER / 余烬', colors: [0xff4400, 0xffaa00, 0x330000] },
WINTER: { name: 'FROST / 霜雪', colors: [0xaaccff, 0xffffff, 0x8899aa] },
LOVE: { name: 'PULSE / 悸动', colors: [0xff0044, 0xff88aa, 0x440011] },
SPIRIT: { name: 'SOUL / 灵光', colors: [0x00ffcc, 0xaa00ff, 0x0000ff] }
};
// 自动主题选择器
function getTheme() {
const m = new Date().getMonth() + 1;
const d = new Date().getDate();
if (m===2 && d===14) return THEMES.LOVE;
if (m===10 && d===31) return THEMES.SPIRIT;
if (m>=3 && m<=5) return THEMES.SPRING;
if (m>=6 && m<=8) return THEMES.SUMMER;
if (m>=9 && m<=11) return THEMES.AUTUMN;
return THEMES.WINTER;
}
const ACTIVE_THEME = getTheme();
/**
* ============================================================================
* 2. 物理核心 (Physics Core) - 重写为基于力的系统
* ============================================================================
*/
class PhysicsSystem {
constructor(scene) {
this.count = CONFIG.particleCount;
this.geometry = new THREE.BufferGeometry();
// 双重缓冲数据Current(当前), Target(回归目标), Velocity(速度)
this.positions = new Float32Array(this.count * 3);
this.origins = new Float32Array(this.count * 3); // 原始位置(用于回归)
this.velocities = new Float32Array(this.count * 3);
this.colors = new Float32Array(this.count * 3);
this.sizes = new Float32Array(this.count);
this.life = new Float32Array(this.count); // 粒子生命周期/闪烁偏移
this.initParticles();
// ShaderMaterial 提供高性能渲染和柔和的光点
this.material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
pixelRatio: { value: window.devicePixelRatio },
baseSize: { value: CONFIG.particleSize }
},
vertexShader: `
attribute float size;
attribute vec3 color;
attribute float life;
varying vec3 vColor;
varying float vAlpha;
uniform float time;
uniform float pixelRatio;
uniform float baseSize;
void main() {
vColor = color;
// 粒子呼吸效果
float breath = 0.6 + 0.4 * sin(time * 2.0 + life * 10.0);
vAlpha = 0.5 + 0.5 * breath;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = baseSize * size * breath * pixelRatio * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vAlpha;
void main() {
// 圆形绘制,边缘羽化
vec2 coord = gl_PointCoord - vec2(0.5);
float r = length(coord);
if (r > 0.5) discard;
// 核心亮,边缘暗
float glow = 1.0 - (r * 2.0);
glow = pow(glow, 1.5);
gl_FragColor = vec4(vColor, glow * vAlpha);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
this.mesh = new THREE.Points(this.geometry, this.material);
scene.add(this.mesh);
}
initParticles() {
// 创建一个无序但均匀的云团
const c1 = new THREE.Color(ACTIVE_THEME.colors[0]);
const c2 = new THREE.Color(ACTIVE_THEME.colors[1]);
const c3 = new THREE.Color(ACTIVE_THEME.colors[2]);
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
// 随机分布在一个宽阔的区域
const x = (Math.random() - 0.5) * 200;
const y = (Math.random() - 0.5) * 120;
const z = (Math.random() - 0.5) * 80;
this.positions[i3] = x; this.positions[i3+1] = y; this.positions[i3+2] = z;
this.origins[i3] = x; this.origins[i3+1] = y; this.origins[i3+2] = z;
// 颜色混合
const rand = Math.random();
let c;
if(rand < 0.33) c = c1;
else if(rand < 0.66) c = c2;
else c = c3;
this.colors[i3] = c.r; this.colors[i3+1] = c.g; this.colors[i3+2] = c.b;
this.sizes[i] = Math.random() * 1.5 + 0.5;
this.life[i] = Math.random();
}
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
this.geometry.setAttribute('life', new THREE.BufferAttribute(this.life, 1));
}
// ★★★ 核心物理更新逻辑 ★★★
update(time, handData) {
this.material.uniforms.time.value = time;
// 获取手部数据 (如果没有手,数组为空)
const hands = handData.hands || [];
for(let i=0; i<this.count; i++) {
const i3 = i * 3;
const px = this.positions[i3];
const py = this.positions[i3+1];
const pz = this.positions[i3+2];
// 1. Curl Noise (旋度噪声) - 让粒子自然流动的关键
// 计算噪声场对速度的影响,模拟空气流动
const noiseScale = 0.015;
const n1 = Simplex.noise3D(px*noiseScale, py*noiseScale, time*0.2);
const n2 = Simplex.noise3D(px*noiseScale + 100, py*noiseScale, time*0.2);
const n3 = Simplex.noise3D(px*noiseScale + 200, py*noiseScale, time*0.2);
// 施加环境流动力
this.velocities[i3] += n1 * 0.02;
this.velocities[i3+1] += n2 * 0.02;
this.velocities[i3+2] += n3 * 0.02;
// 2. 弹性回归力 (Elasticity) - 让粒子即使被扰动也能慢慢飘回原位
// 除非被手“抓”住,否则它们有自己的家
const ox = this.origins[i3];
const oy = this.origins[i3+1];
const oz = this.origins[i3+2];
this.velocities[i3] += (ox - px) * CONFIG.returnSpeed;
this.velocities[i3+1] += (oy - py) * CONFIG.returnSpeed;
this.velocities[i3+2] += (oz - pz) * CONFIG.returnSpeed;
// 3. ★★★ 高级手势交互场 (Hand Interaction Field) ★★★
// 支持任意手势、任意数量的手
for (let h = 0; h < hands.length; h++) {
const hand = hands[h];
// 3.1 掌心力场 (Palm Force)
// 如果手张开:产生基于法线的推力 (Push)
// 如果手握拳:产生引力 (Gravity Well)
const dPx = px - hand.palm.x;
const dPy = py - hand.palm.y;
const dPz = pz - hand.palm.z;
const distSq = dPx*dPx + dPy*dPy + dPz*dPz;
// 交互半径
if (distSq < 1500) {
const dist = Math.sqrt(distSq);
const forceFactor = (1500 - distSq) / 1500; // 0 (边缘) -> 1 (中心)
if (hand.isFist) {
// 握拳:黑洞引力,且带有强烈的旋转
// 吸力
this.velocities[i3] -= dPx * 0.1 * forceFactor;
this.velocities[i3+1] -= dPy * 0.1 * forceFactor;
this.velocities[i3+2] -= dPz * 0.1 * forceFactor;
// 旋转力 (Cross Product with Up vector)
this.velocities[i3] += -dPy * 0.2 * forceFactor;
this.velocities[i3+1] += dPx * 0.2 * forceFactor;
} else {
// 张开:基于手掌法线方向的推力 (空气炮)
// 计算点乘,判断粒子是否在手掌前方
// 简单模拟:径向推开 + 手掌朝向的定向风
const pushStrength = 2.0 * hand.velocity; // 动得越快,风越大
this.velocities[i3] += hand.normal.x * pushStrength * forceFactor;
this.velocities[i3+1] += hand.normal.y * pushStrength * forceFactor;
this.velocities[i3+2] += hand.normal.z * pushStrength * forceFactor;
// 基础斥力 (防止穿模)
if (dist < 10) {
this.velocities[i3] += dPx * 0.5 * forceFactor;
this.velocities[i3+1] += dPy * 0.5 * forceFactor;
this.velocities[i3+2] += dPz * 0.5 * forceFactor;
}
}
}
// 3.2 指尖轨迹 (Finger Trails)
// 每一个指尖都是一个微小的扰动源,允许精细作画
for (let f = 0; f < 5; f++) {
const tip = hand.fingertips[f];
const dtx = px - tip.x;
const dty = py - tip.y;
const dtz = pz - tip.z;
const tipDistSq = dtx*dtx + dty*dty + dtz*dtz;
if (tipDistSq < 100) {
// 指尖产生湍流
this.velocities[i3] += (Math.random()-0.5) * 1.5;
this.velocities[i3+1] += (Math.random()-0.5) * 1.5;
this.velocities[i3+2] += (Math.random()-0.5) * 1.5;
}
}
}
// 4. 积分与阻尼
this.velocities[i3] *= CONFIG.friction;
this.velocities[i3+1] *= CONFIG.friction;
this.velocities[i3+2] *= CONFIG.friction;
this.positions[i3] += this.velocities[i3];
this.positions[i3+1] += this.velocities[i3+1];
this.positions[i3+2] += this.velocities[i3+2];
}
this.geometry.attributes.position.needsUpdate = true;
}
}
/**
* ============================================================================
* 3. 智能感知层 (Intelligent Perception)
* ============================================================================
*/
class HandInterface {
constructor() {
this.handData = { hands: [] };
// MediaPipe 初始化
this.hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
this.hands.setOptions({
maxNumHands: 2,
modelComplexity: 1,
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7
});
this.hands.onResults(this.processResults.bind(this));
// 摄像头启动
const video = document.getElementById('input-video');
const camera = new Camera(video, {
onFrame: async () => { await this.hands.send({image: video}); },
width: 640, height: 480
});
camera.start().then(() => {
// UI 更新
document.getElementById('loader-bar').style.width = '100%';
setTimeout(() => document.getElementById('loader').style.opacity = 0, 500);
setTimeout(() => {
document.getElementById('title').classList.add('visible');
document.getElementById('subtitle').classList.add('visible');
document.getElementById('loader').style.display = 'none';
}, 1000);
});
// 上一帧数据用于计算速度
this.prevHands = {};
}
processResults(results) {
const landmarks = results.multiHandLandmarks;
const worldLandmarks = results.multiHandWorldLandmarks; // 使用真实世界坐标计算法线
const currentHands = [];
const debugEl = document.getElementById('hand-state');
if (landmarks) {
landmarks.forEach((lm, index) => {
// 1. 坐标映射 (2D -> 3D Scene Space)
// 屏幕中心为 (0,0), 范围约 -60 到 60
const palmCenter = lm[9]; // 中指根部作为手掌中心
const pos = new THREE.Vector3(
(1.0 - palmCenter.x) * 140 - 70, // X 镜像翻转
-(palmCenter.y * 100 - 50), // Y 翻转
0 // Z 暂定为0后续可根据 lm.z 优化深度
);
// 2. 计算速度 (Velocity)
let velocity = 0;
if (this.prevHands[index]) {
const dist = pos.distanceTo(this.prevHands[index]);
velocity = Math.min(dist, 5.0); // 限制最大速度
}
// 3. 计算手势状态 (State Analysis)
// 3.1 握拳检测: 比较指尖(tip)到掌心(wrist/center)的距离
const wrist = lm[0];
let foldedFingers = 0;
const tips = [8, 12, 16, 20]; // 4 fingers (excluding thumb)
tips.forEach(t => {
// 简单判断: 如果指尖y坐标 低于 指根y坐标 (屏幕空间),或距离手腕太近
// 更稳健的方法是3D距离
const dTip = Math.hypot(lm[t].x - wrist.x, lm[t].y - wrist.y);
const dPip = Math.hypot(lm[t-2].x - wrist.x, lm[t-2].y - wrist.y);
if (dTip < dPip) foldedFingers++;
});
const isFist = foldedFingers >= 3;
// 3.2 手掌法线 (Normal Vector)
// 利用 P0(Wrist), P5(IndexBase), P17(PinkyBase) 计算平面法线
// 这里做一个简化估算:根据手掌移动方向和手腕-中指向量
const pWrist = new THREE.Vector3((1-lm[0].x), -lm[0].y, 0);
const pMiddle = new THREE.Vector3((1-lm[9].x), -lm[9].y, 0);
const dir = new THREE.Vector3().subVectors(pMiddle, pWrist).normalize();
// 默认法线朝向屏幕外 (0,0,1),结合方向旋转
const normal = new THREE.Vector3(0, 0, 1).add(dir.multiplyScalar(0.5)).normalize();
// 4. 收集指尖位置 (用于精细交互)
const fingertips = [4, 8, 12, 16, 20].map(i => {
return new THREE.Vector3(
(1.0 - lm[i].x) * 140 - 70,
-(lm[i].y * 100 - 50),
(lm[i].z || 0) * 50 // 尝试引入深度
);
});
currentHands.push({
id: index,
palm: pos,
velocity: velocity,
isFist: isFist,
normal: normal,
fingertips: fingertips
});
// 更新上一帧
this.prevHands[index] = pos.clone();
});
}
this.handData.hands = currentHands;
// 更新 UI Debug
if (currentHands.length > 0) {
const h = currentHands[0];
const stateStr = h.isFist ? "GRAVITY WELL (FIST)" : "WIND FORCE (OPEN)";
debugEl.innerHTML = `HANDS: DETECTED (${currentHands.length})<br>MODE: ${stateStr}<br>VEL: ${h.velocity.toFixed(2)}`;
} else {
debugEl.innerHTML = `HANDS: SEARCHING...<br>Please show your hands`;
}
}
}
/**
* ============================================================================
* 4. 渲染循环 (Main Loop)
* ============================================================================
*/
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.01);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 60; // 摄像机拉远,看到更多粒子
const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('canvas-container').appendChild(renderer.domElement);
// Post Processing (Bloom)
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.strength = CONFIG.bloomStrength;
bloomPass.radius = 0.5;
bloomPass.threshold = 0.1;
composer.addPass(bloomPass);
// Init Systems
const physics = new PhysicsSystem(scene);
const perception = new HandInterface();
// Update UI Text
document.getElementById('theme-display').innerText = `THEME: ${ACTIVE_THEME.name}`;
document.getElementById('title').innerText = ACTIVE_THEME.name.split('/')[0];
// Animation Loop
const clock = new THREE.Clock();
let frames = 0;
let lastTime = 0;
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
const delta = clock.getDelta();
// FPS Counter
frames++;
if (time - lastTime >= 1) {
document.getElementById('fps-display').innerText = `FPS: ${frames}`;
frames = 0;
lastTime = time;
}
// Update Physics
physics.update(time, perception.handData);
// Subtle Camera Move
camera.position.x = Math.sin(time * 0.1) * 2;
camera.position.y = Math.cos(time * 0.1) * 2;
camera.lookAt(0,0,0);
composer.render();
}
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
<script defer src="https://events.vercount.one/js"></script>
</body>
</html>