Compare commits
45 Commits
version-2.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e911421f | ||
|
|
d0091fc713 | ||
|
|
dac607472f | ||
|
|
10d73b33e1 | ||
|
|
19e9801318 | ||
|
|
b424f898db | ||
|
|
ad744a0690 | ||
|
|
78329785a7 | ||
|
|
3fb3739666 | ||
|
|
28013c438f | ||
|
|
785bf0fe61 | ||
|
|
cada354dbe | ||
|
|
b5974a4407 | ||
|
|
a7cc299695 | ||
|
|
6b38d01dd3 | ||
|
|
503ae0c273 | ||
|
|
75231ee73a | ||
|
|
31b7d72123 | ||
|
|
2b6d363aba | ||
|
|
e47c5c2803 | ||
|
|
a98496c232 | ||
|
|
1ed730a3d2 | ||
|
|
cff4db87af | ||
|
|
dd837153b8 | ||
|
|
b9461c5741 | ||
|
|
fef7ca2288 | ||
|
|
5268f35af1 | ||
|
|
978b618df2 | ||
|
|
0174d29bde | ||
|
|
7b9d343cf9 | ||
|
|
a2b627b4ed | ||
|
|
0f6504a46b | ||
|
|
5ced418d1a | ||
|
|
67049a126f | ||
|
|
c21d276f40 | ||
|
|
e183f8bf63 | ||
|
|
3031355836 | ||
|
|
0de81219bb | ||
|
|
aca4d5a0de | ||
|
|
2d52212ad1 | ||
|
|
88e1d9e3d9 | ||
|
|
4fe9034fb9 | ||
|
|
6272941f3e | ||
|
|
67b7c0e5f9 | ||
|
|
a871a734ee |
60
about.html
60
about.html
@@ -40,15 +40,16 @@
|
||||
<meta property="wechat:description" content="我是一名充满热情的Java后端开发工程师,专注于AI技术的探索与应用。">
|
||||
|
||||
<!-- 核心资源:使用 BootCDN 加速 -->
|
||||
<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 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'">
|
||||
<!-- Artalk 评论样式 -->
|
||||
<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="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="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);
|
||||
@@ -73,7 +74,7 @@
|
||||
</a>
|
||||
|
||||
<div class="nav-menu">
|
||||
<a href="index.html" class="nav-item">
|
||||
<a href="index.html" class="nav-item" aria-label="Home">
|
||||
<i class="ri-home-smile-2-line"></i>
|
||||
<span class="nav-label" data-i18n="nav.home">首页</span>
|
||||
</a>
|
||||
@@ -81,7 +82,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">
|
||||
<a href="https://blog.hehouhui.cn" class="nav-item" aria-label="Honesty Blog">
|
||||
<i class="ri-quill-pen-line"></i>
|
||||
<span class="nav-label" data-i18n="nav.blog">博客</span>
|
||||
</a>
|
||||
@@ -110,7 +111,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">
|
||||
<img src="images/avatar.jpeg" alt="Honesty" class="avatar-img" loading="lazy" width="120" height="120">
|
||||
<div class="status-dot" data-i18n="status.online">Online</div>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
@@ -129,6 +130,15 @@
|
||||
<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>
|
||||
|
||||
@@ -175,7 +185,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"/>
|
||||
<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"/>
|
||||
</span>
|
||||
</div>
|
||||
<p class="mbti-desc" data-i18n="mbti.desc">"理想主义与道德感,果断决绝的行动力。深度洞察与创意,关怀与同理心。"</p>
|
||||
@@ -301,8 +311,7 @@
|
||||
<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>
|
||||
<!-- 隐藏的音频播放iframe -->
|
||||
<iframe id="audio-player-iframe" src="audio-player.html" style="display: none;"></iframe>
|
||||
<audio id="site-audio" class="site-audio" src="data/至少做一件离谱的事-Kiri T.mp3" autoplay loop preload="auto"></audio>
|
||||
</main>
|
||||
|
||||
<!-- 微信弹窗 -->
|
||||
@@ -311,8 +320,7 @@
|
||||
<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">
|
||||
<!-- <div class="qr-fallback">QR Load Failed</div>-->
|
||||
<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">
|
||||
</div>
|
||||
<p data-i18n="modal.desc">Scan to follow Tech Share</p>
|
||||
</div>
|
||||
@@ -327,24 +335,25 @@
|
||||
</div>
|
||||
|
||||
<!-- 脚本:BootCDN jQuery / Artalk -->
|
||||
<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 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>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 动态加载不蒜子统计脚本
|
||||
const script = document.createElement('script');
|
||||
script.src = SiteConfig.analytics.busuanzi.src;
|
||||
script.src = "//cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.abbr.min.js";
|
||||
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: SiteConfig.analytics.tencent.id, ck: SiteConfig.analytics.tencent.ck});
|
||||
!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"});
|
||||
} catch (e) {
|
||||
console.log("51.la统计错误", e);
|
||||
}
|
||||
@@ -356,7 +365,7 @@
|
||||
(function () {
|
||||
try {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = SiteConfig.analytics.baidu.src;
|
||||
hm.src = "https://hm.baidu.com/hm.js?ae2a009a75b13c21d5121ee51375ea4e";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
} catch (e) {
|
||||
@@ -370,8 +379,8 @@
|
||||
<script>
|
||||
// 监听不蒜子数据的错误兜底
|
||||
function initFormatter() {
|
||||
const pvEl = document.getElementById(SiteConfig.analytics.busuanzi.site_pv_id);
|
||||
const uvEl = document.getElementById(SiteConfig.analytics.busuanzi.site_uv_id);
|
||||
const pvEl = document.getElementById("busuanzi_site_pv");
|
||||
const uvEl = document.getElementById("busuanzi_site_uv");
|
||||
|
||||
if (!pvEl && !uvEl) return;
|
||||
console.log('[Busuanzi]', 'Formatting... Listener observer');
|
||||
@@ -401,5 +410,12 @@
|
||||
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>
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
<!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>
|
||||
1497
christmas.html
Normal file
1497
christmas.html
Normal file
File diff suppressed because it is too large
Load Diff
211
css/about.css
211
css/about.css
@@ -584,8 +584,6 @@ body {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.avatar-ring {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
@@ -638,6 +636,48 @@ 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 {
|
||||
@@ -689,6 +729,40 @@ 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 */
|
||||
@@ -704,10 +778,10 @@ body {
|
||||
background-clip: text !important;
|
||||
color: transparent !important;
|
||||
text-shadow:
|
||||
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);
|
||||
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);
|
||||
transform: translateZ(0);
|
||||
-webkit-text-stroke: 0.3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -725,16 +799,29 @@ body {
|
||||
-webkit-text-stroke: 0.2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Social Links */
|
||||
.social-dock {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
|
||||
|
||||
/* 推荐分享模块 */
|
||||
.recommend-share-module {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
animation: shareModuleFadeIn 0.5s ease-out 0.5s forwards;
|
||||
}
|
||||
|
||||
.s-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
@keyframes shareModuleFadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.share-link {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
display: flex;
|
||||
@@ -743,29 +830,51 @@ body {
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.s-icon:hover {
|
||||
.share-link:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.s-icon:hover i {
|
||||
color: transparent !important;
|
||||
-webkit-text-fill-color: transparent;
|
||||
|
||||
.share-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.s-icon i {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
-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[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 {
|
||||
@@ -785,9 +894,19 @@ 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 */
|
||||
@@ -801,9 +920,8 @@ body {
|
||||
justify-content: space-around;
|
||||
padding: 20px;
|
||||
}
|
||||
.desktop-social {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.ms-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -1005,6 +1123,18 @@ 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 {
|
||||
@@ -1395,8 +1525,8 @@ body {
|
||||
}
|
||||
[data-theme="day"] .tech-tag-3d.tag-color-23,
|
||||
[data-theme="day"] .tech-tag-mobile.tag-color-23 {
|
||||
color: #ffffff !important;
|
||||
-webkit-text-fill-color: #ffffff !important;
|
||||
color: #d57eeb !important;
|
||||
-webkit-text-fill-color: #d57eeb !important;
|
||||
}
|
||||
[data-theme="day"] .tech-tag-3d.tag-color-24,
|
||||
[data-theme="day"] .tech-tag-mobile.tag-color-24 {
|
||||
@@ -2295,12 +2425,7 @@ 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"] {
|
||||
|
||||
@@ -6,15 +6,7 @@
|
||||
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);
|
||||
@@ -450,6 +442,12 @@
|
||||
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;
|
||||
|
||||
BIN
data/christmas.mp3
Normal file
BIN
data/christmas.mp3
Normal file
Binary file not shown.
@@ -15,11 +15,18 @@
|
||||
},
|
||||
{
|
||||
"name": "Home",
|
||||
"stargazers_count": 2,
|
||||
"stargazers_count": 3,
|
||||
"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,
|
||||
@@ -34,6 +41,13 @@
|
||||
"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,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"blog": "https://www.hehouhui.cn",
|
||||
"hireable": true,
|
||||
"bio": "Hi, I’m Honesty—Shanghai Java/AI Dev (7+ yrs). Spring AI, LLM, TensorFlow, Faiss. Write tech content, cycle for inspiration. AI-obsessed.",
|
||||
"public_repos": 165,
|
||||
"public_repos": 167,
|
||||
"public_gists": 0,
|
||||
"followers": 6,
|
||||
"following": 12,
|
||||
|
||||
BIN
data/hand_landmarker.task
Normal file
BIN
data/hand_landmarker.task
Normal file
Binary file not shown.
22
data/wasm/vision_wasm_internal.js
Normal file
22
data/wasm/vision_wasm_internal.js
Normal file
File diff suppressed because one or more lines are too long
BIN
data/wasm/vision_wasm_internal.wasm
Normal file
BIN
data/wasm/vision_wasm_internal.wasm
Normal file
Binary file not shown.
22
data/wasm/vision_wasm_nosimd_internal.js
Normal file
22
data/wasm/vision_wasm_nosimd_internal.js
Normal file
File diff suppressed because one or more lines are too long
BIN
data/wasm/vision_wasm_nosimd_internal.wasm
Normal file
BIN
data/wasm/vision_wasm_nosimd_internal.wasm
Normal file
Binary file not shown.
8
edgeone.json
Normal file
8
edgeone.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/.well-known/*.txt",
|
||||
"destination": "/well-known/:splat.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
index.html
26
index.html
@@ -335,10 +335,7 @@
|
||||
<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 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 defer src="https://events.vercount.one/js"></script>
|
||||
|
||||
<!-- 不蒜子统计 -->
|
||||
<script>
|
||||
@@ -413,26 +410,5 @@
|
||||
}()
|
||||
}({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
215
js/StarrySky.js
@@ -1,215 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}();
|
||||
165
js/about.js
165
js/about.js
@@ -13,7 +13,7 @@ function getStoredLanguage() {
|
||||
|
||||
// 公共方法:设置本地存储的主题设置
|
||||
function setStoredTheme(theme) {
|
||||
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
|
||||
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme';
|
||||
localStorage.setItem(cacheKey, JSON.stringify({
|
||||
value: theme, time: new Date().getTime()
|
||||
}));
|
||||
@@ -534,9 +534,6 @@ class DataManager {
|
||||
=========================== */
|
||||
class UIManager {
|
||||
constructor() {
|
||||
this.userInteracted = false;
|
||||
this.audioPlayer = null;
|
||||
this.audioIframe = null;
|
||||
this.initTechCloud();
|
||||
this.initModal();
|
||||
this.initArtalk();
|
||||
@@ -1083,33 +1080,10 @@ class UIManager {
|
||||
const theme = getStoredTheme();
|
||||
fLang.querySelector('.fab-text').textContent = lang === 'zh' ? 'English' : '中文';
|
||||
fTheme.querySelector('.fab-text').textContent = theme === 'night' ? 'Day' : 'Night';
|
||||
// 音频播放状态需要通过iframe通信获取
|
||||
const audioIframe = document.getElementById('audio-player-iframe');
|
||||
if (audioIframe && audioIframe.contentWindow) {
|
||||
// 请求当前播放状态
|
||||
audioIframe.contentWindow.postMessage({
|
||||
action: 'getCurrentState'
|
||||
}, '*');
|
||||
}
|
||||
const playing = (this.audio && !this.audio.paused);
|
||||
fMusic.querySelector('.fab-text').textContent = lang === 'zh' ? (playing ? '暂停' : '播放') : (playing ? 'Pause' : 'Play');
|
||||
});
|
||||
};
|
||||
|
||||
// 监听来自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');
|
||||
@@ -1127,15 +1101,17 @@ class UIManager {
|
||||
requestAnimationFrame(updateLabels);
|
||||
});
|
||||
fMusic.addEventListener('click', () => {
|
||||
const audioIframe = document.getElementById('audio-player-iframe');
|
||||
if (audioIframe && audioIframe.contentWindow) {
|
||||
// 直接发送播放指令,让iframe内部处理播放/暂停切换
|
||||
audioIframe.contentWindow.postMessage({
|
||||
action: 'play'
|
||||
}, '*');
|
||||
|
||||
// 记录用户操作
|
||||
this.setMusicPauseTime(); // 先记录暂停时间
|
||||
if (this.audio) {
|
||||
if (this.audio.paused) {
|
||||
this.audio.play().catch(() => {
|
||||
});
|
||||
// 清除暂停时间记录,允许下次自动播放
|
||||
this.clearMusicPauseTime();
|
||||
} else {
|
||||
this.audio.pause();
|
||||
// 记录暂停时间
|
||||
this.setMusicPauseTime();
|
||||
}
|
||||
}
|
||||
// 延迟更新标签以避免阻塞
|
||||
requestAnimationFrame(updateLabels);
|
||||
@@ -1146,33 +1122,33 @@ class UIManager {
|
||||
|
||||
|
||||
initAudio() {
|
||||
// 获取已存在的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;
|
||||
const el = document.getElementById('site-audio');
|
||||
if (!el) return;
|
||||
this.audio = el;
|
||||
this.audio.loop = true;
|
||||
|
||||
|
||||
const autoPlayer = () => {
|
||||
// 页面加载完成后根据条件决定是否播放
|
||||
window.addEventListener('load', () => {
|
||||
// 检查是否在24小时内用户暂停过音乐
|
||||
const shouldRemainPaused = this.shouldMusicRemainPaused();
|
||||
// 如果不应该保持暂停状态,则尝试播放
|
||||
if (!shouldRemainPaused) {
|
||||
let userInteracted = true;
|
||||
this.audio.play().catch(() => {
|
||||
// 静默处理播放失败
|
||||
userInteracted = false;
|
||||
});
|
||||
// 添加用户交互检查,避免浏览器阻止自动播放
|
||||
const attemptAutoplay = () => {
|
||||
// 检查是否已有用户交互
|
||||
if (this.userInteracted) {
|
||||
this.playAudio();
|
||||
} else {
|
||||
if (this.userInteracted === false) {
|
||||
// 添加一次性用户交互监听器
|
||||
const enableAudio = () => {
|
||||
this.userInteracted = true;
|
||||
this.playAudio();
|
||||
setTimeout(() => {
|
||||
this.audio.play().catch(() => {
|
||||
});
|
||||
}, 1000);
|
||||
document.removeEventListener('click', enableAudio);
|
||||
document.removeEventListener('touchstart', enableAudio);
|
||||
document.removeEventListener('keydown', enableAudio);
|
||||
@@ -1185,86 +1161,9 @@ class UIManager {
|
||||
document.addEventListener('mousemove', enableAudio, { once: true });
|
||||
}
|
||||
};
|
||||
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');
|
||||
requestAnimationFrame(attemptAutoplay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateCustomStyles(container, theme) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// 配置文件 - 提取自各个JavaScript文件的关键配置
|
||||
// 创建日期: 2025-11-20
|
||||
|
||||
(function() {
|
||||
const SiteConfig = {
|
||||
// bj.js 配置
|
||||
stars: {
|
||||
@@ -43,9 +44,9 @@ const SiteConfig = {
|
||||
|
||||
// 通用缓存键与TTL(毫秒)
|
||||
cacheKeys: {
|
||||
github: { key: 'gh_data_v2', ttlMs: 36000000 },
|
||||
blog: { key: 'blog_data_v2', ttlMs: 3600000 },
|
||||
theme: { key: 'theme_v2', ttlMs: 3600000 }
|
||||
github: { key: 'gh_data', ttlMs: 36000000 },
|
||||
blog: { key: 'blog_data', ttlMs: 3600000 },
|
||||
theme: { key: 'theme', ttlMs: 3600000 }
|
||||
},
|
||||
|
||||
techStack: [
|
||||
@@ -177,3 +178,4 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.SiteConfig = SiteConfig;
|
||||
}
|
||||
})();
|
||||
@@ -1,58 +0,0 @@
|
||||
$(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
Normal file
620
me.html
Normal file
@@ -0,0 +1,620 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user