新增: 星座运势+AI资讯+知识卡片+桌面设置窗口+秒显示开关

- 星座运势: 天聚数行API集成,5维进度条+幸运标签+今日概述
- AI资讯: 天聚数行API,图文布局5条展示,文件缓存2小时刷新
- 知识卡片: AI生成,关键字+提示词配置,30分钟刷新
- 桌面设置: 独立WebView2窗口,760x1350,含壁纸/布局/城市/颜色等配置
- 显示控制: 壁纸/时间/天气/星座/知识/AI资讯独立开关,秒显示开关
- 文件缓存: 星座运势+AI资讯缓存到本地,启动即显示上次数据
- initDone防抖: 防止设置窗口初始化触发卡片重载
This commit is contained in:
2026-05-26 04:34:00 +08:00
parent 2287e12e0d
commit 9fd3acede3
15 changed files with 2607 additions and 384 deletions

View File

@@ -22,7 +22,6 @@ html, body {
border-radius: 20px;
padding: 24px 28px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255,255,255,0.08);
z-index: 10;
}
.time {
@@ -34,6 +33,20 @@ html, body {
font-variant-numeric: tabular-nums;
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
}
@keyframes hourlyGlow {
0% { text-shadow: 0 0 30px rgba(255,255,255,1), 0 0 80px rgba(180,200,255,0.8), 0 0 120px rgba(100,150,255,0.5); color: #fff; }
8% { text-shadow: 0 0 50px rgba(255,255,255,1), 0 0 100px rgba(180,200,255,1), 0 0 160px rgba(100,150,255,0.6); }
18% { text-shadow: 0 0 20px rgba(255,255,255,0.6), 0 0 40px rgba(180,200,255,0.3); }
28% { text-shadow: 0 0 35px rgba(255,255,255,0.85), 0 0 70px rgba(180,200,255,0.6), 0 0 100px rgba(100,150,255,0.35); }
38% { text-shadow: 0 0 15px rgba(255,255,255,0.4), 0 0 30px rgba(180,200,255,0.2); }
48% { text-shadow: 0 0 25px rgba(255,255,255,0.6), 0 0 50px rgba(180,200,255,0.4); }
60% { text-shadow: 0 0 10px rgba(255,255,255,0.25), 0 0 20px rgba(180,200,255,0.12); }
75% { text-shadow: 0 0 15px rgba(255,255,255,0.35), 0 0 30px rgba(180,200,255,0.2); }
100% { text-shadow: 0 2px 20px rgba(0,0,0,0.5); color: #fff; }
}
.time.hourly-glow {
animation: hourlyGlow 6s ease-out forwards;
}
.date {
font-size: 14px;
color: rgba(255,255,255,0.7);
@@ -101,6 +114,152 @@ html, body {
line-height: 1.6;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.zodiac-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 2px;
}
.zodiac-date {
font-size: 11px;
opacity: 0.4;
margin-bottom: 14px;
}
.zodiac-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 7px;
font-size: 12px;
}
.zodiac-bar-label {
width: 28px;
opacity: 0.55;
flex-shrink: 0;
}
.zodiac-bar-track {
flex: 1;
height: 4px;
background: rgba(255,255,255,0.08);
border-radius: 2px;
overflow: hidden;
}
.zodiac-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.6s ease;
}
.zodiac-bar-val {
width: 30px;
text-align: right;
font-size: 11px;
opacity: 0.7;
flex-shrink: 0;
}
.zodiac-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin: 12px 0;
}
.zodiac-tag {
font-size: 10px;
background: rgba(255,255,255,0.06);
padding: 2px 8px;
border-radius: 5px;
opacity: 0.6;
}
.zodiac-summary {
font-size: 12px;
opacity: 0.6;
line-height: 1.7;
}
/* ===== AI 资讯 ===== */
.ainews-header {
font-size: 11px;
font-weight: 500;
color: rgba(255,255,255,0.45);
letter-spacing: 1px;
margin-bottom: 10px;
}
.ainews-item {
display: flex;
gap: 12px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.ainews-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.ainews-img {
width: 80px;
height: 54px;
border-radius: 6px;
object-fit: cover;
flex-shrink: 0;
opacity: 0.85;
}
.ainews-body {
flex: 1;
min-width: 0;
}
.ainews-title-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.ainews-title {
font-size: 13px;
color: rgba(255,255,255,0.9);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.ainews-source {
font-size: 10px;
opacity: 0.4;
flex-shrink: 0;
}
.ainews-desc {
font-size: 11px;
opacity: 0.5;
line-height: 1.5;
margin-top: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===== 知识卡片 ===== */
.knowledge-header {
font-size: 11px;
color: rgba(255,255,255,0.45);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
letter-spacing: 0.5px;
}
.knowledge-keyword-tag {
background: rgba(255,255,255,0.08);
padding: 1px 8px;
border-radius: 4px;
font-size: 10px;
color: rgba(255,255,255,0.6);
}
.knowledge-content {
font-size: 14px;
color: rgba(255,255,255,0.9);
line-height: 1.7;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.divider {
height: 1px;
@@ -131,7 +290,8 @@ body.layout-single #info {
top: 40px;
right: 40px;
text-align: left;
min-width: 320px;
width: calc(50vw - 60px);
z-index: 10;
}
body.layout-single #info .date {
text-align: left;
@@ -147,20 +307,65 @@ body.layout-multi #card-time {
right: 40px;
text-align: left;
min-width: 280px;
z-index: 10;
}
body.layout-multi #card-weather {
body.layout-multi #card-bottom {
position: fixed;
bottom: 80px;
right: 40px;
text-align: right;
min-width: 420px;
width: calc(50vw - 60px);
display: flex;
gap: 16px;
align-items: flex-end;
z-index: 10;
}
body.layout-multi #card-zodiac {
body.layout-multi #card-right-col {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
min-width: 0;
}
body.layout-multi #card-right-col #card-knowledge {
text-align: left;
}
body.layout-multi #card-right-col #card-ainews {
text-align: left;
}
body.layout-multi #card-bottom #card-weather {
text-align: right;
}
body.layout-multi #card-ainews {
position: fixed;
bottom: 80px;
left: 40px;
min-width: 200px;
width: calc(50vw - 80px);
z-index: 10;
}
body.layout-multi #card-zodiac {
position: fixed;
top: 200px;
right: 40px;
width: 280px;
z-index: 10;
}
/* ===== 卡片隐藏 ===== */
body.hide-time #card-time,
body.hide-time #info .time,
body.hide-time #info .date { display: none !important; }
body.hide-weather #card-weather,
body.hide-weather #info .weather-section,
body.hide-weather #info .current-weather,
body.hide-weather #info .forecast-title,
body.hide-weather #info .weather-forecast,
body.hide-weather #info .daily-forecast { display: none !important; }
body.hide-zodiac #card-zodiac,
body.hide-zodiac #info .zodiac-text { display: none !important; }
body.hide-ainews #card-ainews,
body.hide-ainews #info .ainews-section { display: none !important; }
body.hide-knowledge #card-knowledge,
body.hide-knowledge #info .knowledge-section { display: none !important; }
</style>
</head>
<body class="layout-{{LAYOUT}}">
@@ -172,6 +377,16 @@ body.layout-multi #card-zodiac {
<div class="date" id="date">1月1日 周一</div>
<div class="time" id="time">00:00</div>
<div class="divider"></div>
<div class="ainews-section">
<div class="ainews-header">🤖 AI 资讯</div>
<div id="ainews">加载中...</div>
</div>
<div class="divider"></div>
<div class="knowledge-section">
<div class="knowledge-header">💡 知识卡片 <span class="knowledge-keyword-tag" id="knowledgeTag"></span></div>
<div class="knowledge-content" id="knowledge">请设置知识关键字</div>
</div>
<div class="divider"></div>
<div>
<div class="current-weather" id="currentWeather">加载中...</div>
<div class="forecast-title">24小时预报</div>
@@ -190,38 +405,53 @@ body.layout-multi #card-zodiac {
<div class="date" id="date2">1月1日 周一</div>
<div class="time" id="time2">00:00</div>
</div>
<div id="card-weather" class="card">
<div class="current-weather" id="currentWeather2">加载中...</div>
<div class="forecast-title">24小时预报</div>
<div class="weather-forecast" id="hourlyForecast2"></div>
<div class="forecast-title" style="margin-top:12px">7日预报</div>
<div class="daily-forecast" id="dailyForecast2"></div>
</div>
<div id="card-zodiac" class="card">
<div class="zodiac-text" id="zodiac2">加载中...</div>
</div>
<div id="card-ainews" class="card">
<div class="ainews-header">🤖 AI 资讯</div>
<div id="ainews2">加载中...</div>
</div>
<div id="card-bottom">
<div id="card-right-col">
<div id="card-knowledge" class="card">
<div class="knowledge-header">💡 知识卡片 <span class="knowledge-keyword-tag" id="knowledgeTag2"></span></div>
<div class="knowledge-content" id="knowledge2">请设置知识关键字</div>
</div>
<div id="card-weather" class="card">
<div class="current-weather" id="currentWeather2">加载中...</div>
<div class="forecast-title">24小时预报</div>
<div class="weather-forecast" id="hourlyForecast2"></div>
<div class="forecast-title" style="margin-top:12px">7日预报</div>
<div class="daily-forecast" id="dailyForecast2"></div>
</div>
</div>
</div>
</div>
<div id="author">绝尘</div>
<script>
var lastTimeStr='', lastDateStr='', lastZodiac='';
var horoscopeInfo=null;
var zodiacData = {
'白羊座':{icon:'♈',date:'3.21-4.19',fortune:'今日运势旺盛,适合开展新计划。'},
'金牛座':{icon:'♉',date:'4.20-5.20',fortune:'财运不错,但需注意健康。'},
'双子座':{icon:'♊',date:'5.21-6.21',fortune:'人际关系活跃,社交运势佳。'},
'巨蟹座':{icon:'♋',date:'6.22-7.22',fortune:'情绪敏感,适合独处思考。'},
'狮子座':{icon:'♌',date:'7.23-8.22',fortune:'自信爆棚,工作表现突出。'},
'处女座':{icon:'♍',date:'8.23-9.22',fortune:'细节决定成败,专注当下。'},
'天秤座':{icon:'♎',date:'9.23-10.23',fortune:'感情运佳,单身者有机会。'},
'天蝎座':{icon:'♏',date:'10.24-11.22',fortune:'直觉敏锐,适合做决策。'},
'射手座':{icon:'♐',date:'11.23-12.21',fortune:'冒险精神旺盛,出行注意安全。'},
'摩羯座':{icon:'♑',date:'12.22-1.19',fortune:'事业运佳,工作效率高。'},
'水瓶座':{icon:'♒',date:'1.20-2.18',fortune:'创新思维活跃,灵感不断。'},
'双鱼座':{icon:'♓',date:'2.19-3.20',fortune:'艺术灵感丰富,适合创作。'}
'白羊座':{icon:'♈',date:'3.21-4.19'},
'金牛座':{icon:'♉',date:'4.20-5.20'},
'双子座':{icon:'♊',date:'5.21-6.21'},
'巨蟹座':{icon:'♋',date:'6.22-7.22'},
'狮子座':{icon:'♌',date:'7.23-8.22'},
'处女座':{icon:'♍',date:'8.23-9.22'},
'天秤座':{icon:'♎',date:'9.23-10.23'},
'天蝎座':{icon:'♏',date:'10.24-11.22'},
'射手座':{icon:'♐',date:'11.23-12.21'},
'摩羯座':{icon:'♑',date:'12.22-1.19'},
'水瓶座':{icon:'♒',date:'1.20-2.18'},
'双鱼座':{icon:'♓',date:'2.19-3.20'}
};
var barColors={all:'#e0e0e0',love:'#ff6b9d',work:'#4fc3f7',money:'#ffd54f',health:'#81c784'};
function getUserZodiac(){ return window.userZodiac||'射手座'; }
function setEl(id,html){
@@ -233,14 +463,52 @@ function setText(id,txt){
if(e) e.textContent=txt;
}
function buildBar(label,val,color){
var v=parseInt(val)||0;
return '<div class="zodiac-bar">'+
'<span class="zodiac-bar-label">'+label+'</span>'+
'<div class="zodiac-bar-track"><div class="zodiac-bar-fill" style="width:'+v+'%;background:'+color+'"></div></div>'+
'<span class="zodiac-bar-val">'+val+'%</span></div>';
}
function buildZodiacHTML(name){
var z=zodiacData[name]||{icon:'✨',date:''};
var html='<div class="zodiac-title">'+z.icon+' '+name+'运势</div>';
html+='<div class="zodiac-date">'+z.date+'</div>';
if(horoscopeInfo&&horoscopeInfo.zodiac===name){
html+=buildBar('综合',horoscopeInfo.all,barColors.all);
html+=buildBar('爱情',horoscopeInfo.love,barColors.love);
html+=buildBar('工作',horoscopeInfo.work,barColors.work);
html+=buildBar('财运',horoscopeInfo.money,barColors.money);
html+=buildBar('健康',horoscopeInfo.health,barColors.health);
html+='<div class="zodiac-tags">';
if(horoscopeInfo.luckyColor) html+='<span class="zodiac-tag">🎨 '+horoscopeInfo.luckyColor+'</span>';
if(horoscopeInfo.luckyNum) html+='<span class="zodiac-tag">🔢 '+horoscopeInfo.luckyNum+'</span>';
if(horoscopeInfo.noble) html+='<span class="zodiac-tag">⭐ '+horoscopeInfo.noble+'</span>';
html+='</div>';
if(horoscopeInfo.summary) html+='<div class="zodiac-summary">'+horoscopeInfo.summary+'</div>';
} else {
html+='<div style="opacity:0.4;font-size:12px;margin-top:8px">运势加载中...</div>';
}
return html;
}
function syncZodiacHeight(){
var zw=document.getElementById('card-zodiac');
var ww=document.getElementById('card-weather');
if(!zw||!ww||!document.body.classList.contains('layout-multi')) return;
var wh=ww.getBoundingClientRect().height;
zw.style.minHeight=wh+'px';
}
function updateZodiacDisplay(){
var name=getUserZodiac();
if(name===lastZodiac) return;
lastZodiac=name;
var z=zodiacData[name]||{icon:'✨',date:'',fortune:'运势平稳,保持平常心。'};
var html=z.icon+' '+name+'运势 <span style="opacity:0.4;font-size:12px">'+z.date+'</span><br><span style="opacity:0.6;font-size:12px">'+z.fortune+'</span>';
var html=buildZodiacHTML(name);
setEl('zodiac',html);
setEl('zodiac2',html);
syncZodiacHeight();
}
var holidays=[
@@ -266,12 +534,29 @@ function getNextHoliday(now){
var target=new Date(y,h.m-1,h.d);
var diff=Math.ceil((target-now)/(1000*60*60*24));
if(diff>0&&diff<=60) results.push({diff:diff,name:h.name});
if(diff<0){target=new Date(y+1,h.m-1,h.d);diff=Math.ceil((target-now)/(1000*60*60*24));if(diff>0&&diff<=60)results.push({diff:diff,name:h.name});}
if(diff<0){
target=new Date(y+1,h.m-1,h.d);
diff=Math.ceil((target-now)/(1000*60*60*24));
if(diff>0&&diff<=60) results.push({diff:diff,name:h.name});
}
}
results.sort(function(a,b){return a.diff-b.diff;});
return results.length>0?'距'+results[0].name+'还有'+results[0].diff+'天':'';
}
window.setCardVisible=function(card,visible){
if(visible){document.body.classList.remove('hide-'+card);}
else{document.body.classList.add('hide-'+card);}
};
window.setWallpaperVisible=function(visible){
var bg=document.getElementById('bg-layer');
if(bg){bg.style.display=visible?'':'none';}
};
window._showSeconds={{SHOW_SECONDS}};
window.setShowSeconds=function(v){window._showSeconds=v; updateTime();};
function updateTime(){
var now=new Date();
var hh=String(now.getHours()).padStart(2,'0');
@@ -280,10 +565,21 @@ function updateTime(){
var month=now.getMonth()+1;
var day=now.getDate();
var week=['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
var timeStr=hh+':'+mm+':'+ss;
var timeStr=window._showSeconds?(hh+':'+mm+':'+ss):(hh+':'+mm);
var dateStr=month+'月'+day+'日 '+week;
if(timeStr!==lastTimeStr){
setText('time',timeStr); setText('time2',timeStr); lastTimeStr=timeStr;
if(mm==='00'&&ss==='00'){
['time','time2'].forEach(function(id){
var el=document.getElementById(id);
if(el){
el.classList.remove('hourly-glow');
void el.offsetWidth;
el.classList.add('hourly-glow');
el.addEventListener('animationend',function(){ el.classList.remove('hourly-glow'); },{once:true});
}
});
}
}
if(dateStr!==lastDateStr){
var holiday=getNextHoliday(now);
@@ -293,6 +589,50 @@ function updateTime(){
updateZodiacDisplay();
}
window.updateHoroscopeFromGo=function(data){
console.log('[horoscope] received:', typeof data, JSON.stringify(data).substring(0,100));
if(typeof data==='string') data=JSON.parse(data);
horoscopeInfo=data;
lastZodiac='';
window.userZodiac=data.zodiac;
updateZodiacDisplay();
syncZodiacHeight();
};
window.updateAINewsFromGo=function(items){
if(typeof items==='string') items=JSON.parse(items);
if(!items||!items.length) return;
var html='';
var count=Math.min(items.length,5);
for(var i=0;i<count;i++){
var n=items[i];
var time=n.ctime||'';
if(time.length>10) time=time.substring(5,10);
html+='<div class="ainews-item">';
if(n.picUrl){
html+='<img class="ainews-img" src="'+n.picUrl+'" loading="lazy" onerror="this.style.display=\'none\'">';
}
html+='<div class="ainews-body">';
html+='<div class="ainews-title-row"><span class="ainews-title">'+n.title+'</span><span class="ainews-source">'+n.source+' · '+time+'</span></div>';
if(n.description) html+='<div class="ainews-desc">'+n.description+'</div>';
html+='</div></div>';
}
setEl('ainews',html);
setEl('ainews2',html);
};
window.updateKnowledgeFromGo=function(data){
if(typeof data==='string') data=JSON.parse(data);
var ids=['knowledge','knowledge2'];
var tagIds=['knowledgeTag','knowledgeTag2'];
ids.forEach(function(id,i){
var el=document.getElementById(id);
if(el) el.textContent=data.content||'';
var tag=document.getElementById(tagIds[i]);
if(tag) tag.textContent=data.keyword?'#'+data.keyword:'';
});
};
window.updateWeatherFromGo=function(data){
if(typeof data==='string') data=JSON.parse(data);
@@ -318,6 +658,7 @@ window.updateWeatherFromGo=function(data){
renderWeather('currentWeather','hourlyForecast','dailyForecast');
renderWeather('currentWeather2','hourlyForecast2','dailyForecast2');
syncZodiacHeight();
};
updateTime();

699
web/settings.html Normal file
View File

@@ -0,0 +1,699 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg: #0f0f1a;
--card-bg: rgba(255,255,255,0.04);
--card-border: rgba(255,255,255,0.06);
--card-divider: rgba(255,255,255,0.04);
--text: #e0e0e0;
--text-strong: #fff;
--text-weak: rgba(255,255,255,0.25);
--text-muted: rgba(255,255,255,0.5);
--input-bg: rgba(255,255,255,0.08);
--input-border: rgba(255,255,255,0.1);
--input-border-focus: #4f8cff;
--accent: #4f8cff;
--toggle-track: rgba(255,255,255,0.1);
--toggle-thumb: rgba(255,255,255,0.5);
--option-bg: #1a1a2e;
--footer-color: rgba(255,255,255,0.3);
}
.light {
--bg: #f5f5f5;
--card-bg: rgba(0,0,0,0.03);
--card-border: rgba(0,0,0,0.08);
--card-divider: rgba(0,0,0,0.05);
--text: #333;
--text-strong: #111;
--text-weak: rgba(0,0,0,0.35);
--text-muted: rgba(0,0,0,0.55);
--input-bg: rgba(0,0,0,0.04);
--input-border: rgba(0,0,0,0.12);
--input-border-focus: #2b6cb0;
--accent: #2b6cb0;
--toggle-track: rgba(0,0,0,0.12);
--toggle-thumb: rgba(0,0,0,0.45);
--option-bg: #fff;
--footer-color: rgba(0,0,0,0.35);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
padding: 16px 18px;
user-select: none;
-webkit-font-smoothing: antialiased;
overflow-y: auto;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
.light ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); }
.light ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.22); }
.header { margin-bottom: 14px; }
.header h1 { font-size: 16px; font-weight: 600; color: var(--text-strong); }
.header p { font-size: 11px; color: var(--text-weak); margin-top: 2px; }
.section { margin-bottom: 12px; }
.section-label {
font-size: 10px; font-weight: 600; color: var(--text-weak);
text-transform: uppercase; letter-spacing: 1.5px;
margin-bottom: 4px; padding-left: 2px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
}
.item {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 14px;
}
.item + .item { border-top: 1px solid var(--card-divider); }
.item-label { font-size: 12px; font-weight: 500; color: var(--text-muted); }
.item-desc { font-size: 10px; color: var(--text-weak); margin-top: 1px; }
.item-sub { padding-left: 32px; }
/* Toggle */
.switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.switch input { display: none; }
.switch .track {
position: absolute; inset: 0;
background: var(--toggle-track);
border-radius: 10px; cursor: pointer; transition: background 0.2s;
}
.switch .thumb {
position: absolute; width: 14px; height: 14px; top: 3px; left: 3px;
background: var(--toggle-thumb);
border-radius: 50%; transition: all 0.2s; pointer-events: none;
}
.switch input:checked + .track { background: var(--accent); }
.switch input:checked + .track .thumb { transform: translateX(16px); background: var(--text-strong); }
/* Select */
select {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 3px 6px;
font-family: inherit; cursor: pointer; outline: none;
min-width: 80px; max-width: 160px;
}
select:focus { border-color: var(--input-border-focus); }
select option { background: var(--option-bg); color: var(--text); }
/* Button */
.btn {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 4px 10px;
font-family: inherit; cursor: pointer; transition: background 0.15s; white-space: nowrap;
}
.btn:hover { background: var(--card-border); }
.btn:active { background: var(--input-bg); }
.btn.active { background: var(--accent); border-color: var(--accent); color: var(--text-strong); }
.btn-sm { padding: 3px 8px; font-size: 10px; }
.btn-group { display: flex; gap: 4px; }
.radio-tabs { display: flex; flex-wrap: wrap; gap: 4px; }
.bing-nav { display: flex; gap: 4px; align-items: center; }
.wp-type-section { display: none; }
.wp-type-section.visible { display: block; }
input[type="text"] {
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; color: var(--text); font-size: 11px; padding: 4px 8px;
font-family: inherit; outline: none; width: 140px;
}
input[type="text"]:focus { border-color: var(--input-border-focus); }
/* City Picker */
.city-picker {
position: relative;
background: var(--input-bg); border: 1px solid var(--input-border);
border-radius: 6px; padding: 4px 24px 4px 8px; cursor: pointer;
font-size: 11px; min-width: 140px; max-width: 180px;
}
.city-picker:focus, .city-picker.open { border-color: var(--input-border-focus); }
.city-picker-text { color: var(--text); }
.city-picker-arrow {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
color: var(--text-weak); font-size: 10px; pointer-events: none;
}
.city-panel {
display: none; position: absolute; right: -1px; bottom: calc(100% + 4px);
background: var(--option-bg); border: 1px solid var(--input-border);
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25);
z-index: 1000; width: 260px; height: 200px; overflow: hidden;
}
.city-picker.open .city-panel { display: flex; }
.city-col {
flex: 1; overflow-y: auto; border-right: 1px solid var(--card-divider);
}
.city-col:last-child { border-right: none; }
.city-col div {
padding: 5px 10px; font-size: 11px; color: var(--text); cursor: pointer;
white-space: nowrap;
}
.city-col div:hover { background: var(--input-bg); }
.city-col div.active { color: var(--accent); font-weight: 600; }
/* Saved colors */
.color-swatch {
width: 28px; height: 28px; border-radius: 6px; cursor: pointer;
border: 2px solid transparent; transition: border-color 0.15s; position: relative;
}
.color-swatch:hover { border-color: var(--accent); }
.color-swatch .del {
display: none; position: absolute; top: -6px; right: -6px;
width: 14px; height: 14px; border-radius: 50%;
background: #e53e3e; color: #fff; font-size: 9px; line-height: 14px;
text-align: center; cursor: pointer;
}
.color-swatch:hover .del { display: block; }
.footer {
text-align: center; font-size: 11px; color: var(--footer-color); margin-top: 12px;
padding: 6px 0; letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="header">
<h1>桌面设置</h1>
<p>壁纸 · 布局 · 信息显示</p>
</div>
<!-- 显示控制 -->
<div class="section">
<div class="section-label">显示控制</div>
<div class="card">
<div class="item">
<div><div class="item-label">显示壁纸</div></div>
<label class="switch"><input type="checkbox" id="wallpaper" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">时间日期</div></div>
<label class="switch"><input type="checkbox" id="time" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item item-sub">
<div><div class="item-label">显示秒</div></div>
<label class="switch"><input type="checkbox" id="showSeconds" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">天气信息</div></div>
<label class="switch"><input type="checkbox" id="weather" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">星座运势</div></div>
<label class="switch"><input type="checkbox" id="zodiacCard" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">知识卡片</div></div>
<label class="switch"><input type="checkbox" id="knowledgeCard" checked><span class="track"><span class="thumb"></span></span></label>
</div>
<div class="item">
<div><div class="item-label">AI 资讯</div></div>
<label class="switch"><input type="checkbox" id="ainewsCard" checked><span class="track"><span class="thumb"></span></span></label>
</div>
</div>
</div>
<!-- 壁纸类型 -->
<div class="section">
<div class="section-label">壁纸类型</div>
<div class="card">
<div class="item">
<div class="item-label">类型选择</div>
<div class="radio-tabs" id="wpTypeTabs">
<button class="btn" data-type="theme">主题</button>
<button class="btn" data-type="image">本地图片</button>
<button class="btn" data-type="bing">Bing</button>
<button class="btn" data-type="color">纯色/渐变</button>
</div>
</div>
</div>
</div>
<!-- 主题选择 -->
<div class="section wp-type-section" id="sec-theme">
<div class="section-label">壁纸主题</div>
<div class="card">
<div class="item">
<div class="item-label">选择主题</div>
<select id="themeSelect"></select>
</div>
<div class="item" id="textInputRow" style="display:none">
<div class="item-label">自定义文字</div>
<input type="text" id="wallpaperText" placeholder="输入显示文字">
</div>
</div>
</div>
<!-- 本地图片 -->
<div class="section wp-type-section" id="sec-image">
<div class="section-label">本地图片</div>
<div class="card">
<div class="item">
<div class="item-desc" id="imagePathDisplay">未选择图片</div>
<button class="btn" id="btnPickImage">选择图片</button>
</div>
</div>
</div>
<!-- Bing -->
<div class="section wp-type-section" id="sec-bing">
<div class="section-label">Bing 每日壁纸</div>
<div class="card">
<div class="item">
<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:0">
<img id="bingPreview" style="width:64px;height:40px;object-fit:cover;border-radius:4px;flex-shrink:0;display:none">
<div style="min-width:0;overflow:hidden">
<div class="item-label" id="bingCopyright" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">Bing 每日壁纸</div>
<div class="item-desc" id="bingIdx"></div>
</div>
</div>
<div class="bing-nav" style="flex-shrink:0">
<button class="btn btn-sm" id="btnBingPrev"></button>
<button class="btn btn-sm" id="btnBingNext"></button>
<button class="btn btn-sm" id="btnBingFav"></button>
</div>
</div>
<div class="item">
<div><div class="item-label">定时切换</div><div class="item-desc">每小时自动切换壁纸</div></div>
<label class="switch"><input type="checkbox" id="bingAutoRefresh"><span class="track"><span class="thumb"></span></span></label>
</div>
</div>
<div id="bingFavSection" style="display:none;margin-top:6px">
<div class="section-label">收藏列表</div>
<div class="card" id="bingFavList"></div>
</div>
</div>
<!-- 纯色/渐变 -->
<div class="section wp-type-section" id="sec-color">
<div class="section-label">纯色 / 渐变</div>
<div class="card">
<div class="item">
<div class="item-label">选择颜色</div>
<div class="btn-group">
<button class="btn" id="btnSolidColor">纯色</button>
<button class="btn" id="btnGradientColor">渐变</button>
</div>
</div>
<div class="item" id="currentColorRow" style="display:none">
<div class="item-label">当前颜色</div>
<div style="display:flex;align-items:center;gap:6px">
<span id="currentColorPreview" style="display:inline-block;width:24px;height:16px;border-radius:3px;border:1px solid var(--input-border)"></span>
<button class="btn btn-sm" id="btnSaveColor">收藏</button>
</div>
</div>
</div>
<div id="savedColorsSection" style="display:none;margin-top:6px">
<div class="card" id="savedColorsGrid" style="padding:8px 12px;display:flex;flex-wrap:wrap;gap:6px">
</div>
</div>
</div>
<!-- 布局 -->
<div class="section">
<div class="section-label">布局</div>
<div class="card">
<div class="item">
<div class="item-label">信息布局</div>
<select id="layout">
<option value="single">合并卡片</option>
<option value="multi">独立卡片</option>
</select>
</div>
</div>
</div>
<!-- 星座 + 城市 -->
<div class="section">
<div class="section-label">个性化</div>
<div class="card">
<div class="item">
<div class="item-label">我的星座</div>
<select id="zodiacSelect">
<option>白羊座</option><option>金牛座</option><option>双子座</option>
<option>巨蟹座</option><option>狮子座</option><option>处女座</option>
<option>天秤座</option><option>天蝎座</option><option>射手座</option>
<option>摩羯座</option><option>水瓶座</option><option>双鱼座</option>
</select>
</div>
<div class="item">
<div><div class="item-label">知识关键字</div><div class="item-desc">AI 将根据关键字生成知识小卡片</div></div>
<input type="text" id="knowledgeKeyword" placeholder="如: 历史、科学、冷知识">
</div>
<div class="item">
<div><div class="item-label">知识提示词</div><div class="item-desc">自定义生成风格,不会显示在桌面</div></div>
<input type="text" id="knowledgePrompt" placeholder="如: 用幽默口吻、面向程序员">
</div>
<div class="item" style="position:relative">
<div class="item-label">天气城市</div>
<div class="city-picker" id="cityPicker" tabindex="0">
<span class="city-picker-text" id="cityPickerText">未选择</span>
<span class="city-picker-arrow"></span>
<div class="city-panel" id="cityPanel">
<div class="city-col" id="provCol"></div>
<div class="city-col" id="cityCol"></div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">u-desktop v1.0</div>
<script>
var toggleKeys = ['wallpaper', 'time', 'showSeconds', 'weather', 'zodiacCard', 'knowledgeCard', 'ainewsCard'];
var initDone = false;
function sendToggle() {
if (!initDone) return;
var data = {};
toggleKeys.forEach(function(k) { data[k] = document.getElementById(k).checked; });
if (window.saveToggles) window.saveToggles(JSON.stringify(data));
}
toggleKeys.forEach(function(k) {
document.getElementById(k).addEventListener('change', sendToggle);
});
document.getElementById('layout').addEventListener('change', function() {
if (window.saveLayout) window.saveLayout(this.value);
});
document.getElementById('zodiacSelect').addEventListener('change', function() {
if (window.saveZodiac) window.saveZodiac(this.value);
});
var kwTimer = null;
document.getElementById('knowledgeKeyword').addEventListener('input', function() {
clearTimeout(kwTimer);
var val = this.value;
kwTimer = setTimeout(function() {
if (window.saveKnowledgeKeyword) window.saveKnowledgeKeyword(val);
}, 500);
});
var kpTimer = null;
document.getElementById('knowledgePrompt').addEventListener('input', function() {
clearTimeout(kpTimer);
var val = this.value;
kpTimer = setTimeout(function() {
if (window.saveKnowledgePrompt) window.saveKnowledgePrompt(val);
}, 500);
});
// City picker
var cityMap = {};
var selectedCityId = '';
var selectedCityName = '';
var picker = document.getElementById('cityPicker');
var pickerText = document.getElementById('cityPickerText');
var provCol = document.getElementById('provCol');
var cityCol = document.getElementById('cityCol');
var activeProv = '';
function renderProvinces(provinces) {
provCol.innerHTML = '';
provinces.forEach(function(p) {
var div = document.createElement('div');
div.textContent = p;
if (p === activeProv) div.className = 'active';
div.addEventListener('click', function(e) {
e.stopPropagation();
activeProv = p;
renderProvinces(provinces);
renderCities(p);
});
provCol.appendChild(div);
});
}
function renderCities(prov) {
cityCol.innerHTML = '';
var list = cityMap[prov] || [];
list.forEach(function(c) {
var div = document.createElement('div');
div.textContent = c.name;
if (c.id === selectedCityId) div.className = 'active';
div.addEventListener('click', function(e) {
e.stopPropagation();
selectedCityId = c.id;
selectedCityName = c.name;
pickerText.textContent = activeProv + ' · ' + c.name;
picker.classList.remove('open');
renderCities(prov);
if (window.saveCity) window.saveCity(c.id);
});
cityCol.appendChild(div);
});
}
picker.addEventListener('click', function(e) {
e.stopPropagation();
this.classList.toggle('open');
this.focus();
});
document.addEventListener('click', function(e) {
if (!picker.contains(e.target)) picker.classList.remove('open');
});
document.getElementById('themeSelect').addEventListener('change', function() {
if (window.saveWallpaperType) window.saveWallpaperType('theme', this.value);
document.getElementById('textInputRow').style.display = this.value === 'text' ? 'flex' : 'none';
});
var textTimer = null;
document.getElementById('wallpaperText').addEventListener('input', function() {
clearTimeout(textTimer);
var val = this.value;
textTimer = setTimeout(function() {
if (window.saveWallpaperText) window.saveWallpaperText(val);
}, 500);
});
var wpTypeTabs = document.querySelectorAll('#wpTypeTabs .btn');
var currentWpType = '';
function setWpType(type) {
wpTypeTabs.forEach(function(b) { b.classList.toggle('active', b.dataset.type === type); });
document.querySelectorAll('.wp-type-section').forEach(function(s) { s.classList.remove('visible'); });
var sec = document.getElementById('sec-' + type);
if (sec) sec.classList.add('visible');
if (type === 'bing' && currentWpType !== 'bing') {
if (window.enableBing) window.enableBing();
}
currentWpType = type;
}
wpTypeTabs.forEach(function(b) {
b.addEventListener('click', function() { setWpType(b.dataset.type); });
});
document.getElementById('btnPickImage').addEventListener('click', function() {
if (!window.pickLocalImage) return;
window.pickLocalImage().then(function(path) {
if (path) document.getElementById('imagePathDisplay').textContent = path;
});
});
function updateBingUI(stateJson) {
if (!stateJson) return;
var s = JSON.parse(stateJson);
document.getElementById('btnBingFav').textContent = s.label;
document.getElementById('bingCopyright').textContent = s.copyright || 'Bing 每日壁纸';
document.getElementById('bingIdx').textContent = (s.total > 0) ? ((s.idx + 1) + ' / ' + s.total) : '';
// load preview thumbnail
if (s.filename && window.bingThumbDataURI) {
var preview = document.getElementById('bingPreview');
window.bingThumbDataURI(s.filename).then(function(uri) {
if (uri) { preview.src = uri; preview.style.display = 'block'; }
});
}
}
function loadBingFavorites() {
if (!window.getBingFavorites) return;
window.getBingFavorites().then(function(json) {
var favs = JSON.parse(json);
var sec = document.getElementById('bingFavSection');
var list = document.getElementById('bingFavList');
list.innerHTML = '';
if (!favs || favs.length === 0) { sec.style.display = 'none'; return; }
sec.style.display = 'block';
list.style.cssText = 'padding:8px 12px;display:flex;flex-wrap:wrap;gap:6px';
favs.forEach(function(f) {
var img = document.createElement('img');
img.style.cssText = 'width:64px;height:40px;object-fit:cover;border-radius:4px;cursor:pointer;border:2px solid transparent;transition:border-color 0.15s';
img.title = f.copyright + ' (' + f.date + ')';
img.addEventListener('click', function() {
if (window.bingSetByIdx) window.bingSetByIdx(f.idx).then(function(s) { updateBingUI(s); });
});
img.addEventListener('mouseenter', function() { this.style.borderColor = 'var(--accent)'; });
img.addEventListener('mouseleave', function() { this.style.borderColor = 'transparent'; });
list.appendChild(img);
if (window.bingThumbDataURI) {
window.bingThumbDataURI(f.filename).then(function(uri) {
if (uri) img.src = uri;
});
}
});
});
}
document.getElementById('btnBingPrev').addEventListener('click', function() {
if (window.bingNext) window.bingNext().then(function(s) { updateBingUI(s); });
});
document.getElementById('btnBingNext').addEventListener('click', function() {
if (window.bingPrev) window.bingPrev().then(function(s) { updateBingUI(s); });
});
document.getElementById('btnBingFav').addEventListener('click', function() {
if (window.bingToggleFavorite) {
window.bingToggleFavorite().then(function(s) { updateBingUI(s); loadBingFavorites(); });
}
});
document.getElementById('bingAutoRefresh').addEventListener('change', function() {
if (window.saveBingAutoRefresh) window.saveBingAutoRefresh(this.checked);
});
document.getElementById('btnSolidColor').addEventListener('click', function() {
if (window.pickSolidColor) {
window.pickSolidColor().then(function(c) {
if (c) { currentColor1 = c; currentColor2 = ''; currentGradient = false; updateColorPreview(); }
});
}
});
document.getElementById('btnGradientColor').addEventListener('click', function() {
if (window.pickGradientColor) {
window.pickGradientColor().then(function(res) {
if (res) {
var parts = res.split(',');
currentColor1 = parts[0]; currentColor2 = parts[1] || ''; currentGradient = true;
updateColorPreview();
}
});
}
});
// Color favorites
var currentColor1 = '', currentColor2 = '', currentGradient = false;
var savedColors = [];
function updateColorPreview() {
var row = document.getElementById('currentColorRow');
var preview = document.getElementById('currentColorPreview');
if (!currentColor1) { row.style.display = 'none'; return; }
row.style.display = 'flex';
if (currentGradient && currentColor2) {
preview.style.background = 'linear-gradient(135deg,' + currentColor1 + ',' + currentColor2 + ')';
} else {
preview.style.background = currentColor1;
}
}
function renderSavedColors() {
var sec = document.getElementById('savedColorsSection');
var grid = document.getElementById('savedColorsGrid');
grid.innerHTML = '';
if (!savedColors || savedColors.length === 0) { sec.style.display = 'none'; return; }
sec.style.display = 'block';
savedColors.forEach(function(sc, idx) {
var swatch = document.createElement('div');
swatch.className = 'color-swatch';
if (sc.gradient && sc.color2) {
swatch.style.background = 'linear-gradient(135deg,' + sc.color1 + ',' + sc.color2 + ')';
} else {
swatch.style.background = sc.color1;
}
var del = document.createElement('span');
del.className = 'del'; del.textContent = 'x';
del.addEventListener('click', function(e) {
e.stopPropagation();
if (window.removeSavedColor) window.removeSavedColor(idx).then(function() {
savedColors.splice(idx, 1);
renderSavedColors();
});
});
swatch.appendChild(del);
swatch.addEventListener('click', function() {
if (window.applySavedColor) window.applySavedColor(idx);
currentColor1 = sc.color1; currentColor2 = sc.color2; currentGradient = sc.gradient;
updateColorPreview();
});
grid.appendChild(swatch);
});
}
document.getElementById('btnSaveColor').addEventListener('click', function() {
if (!currentColor1) return;
if (window.addSavedColor) window.addSavedColor(currentColor1, currentColor2, currentGradient).then(function() {
savedColors.push({color1: currentColor1, color2: currentColor2, gradient: currentGradient});
renderSavedColors();
});
});
if (window.loadAllSettings) {
window.loadAllSettings().then(function(raw) {
var s = JSON.parse(raw);
// Apply system theme
if (s.lightTheme) {
document.documentElement.className = 'light';
}
// Province-city cascade
cityMap = s.citiesByProv || {};
selectedCityId = s.city || '';
activeProv = '';
if (s.provinces && s.provinces.length) {
for (var p in cityMap) {
for (var ci = 0; ci < cityMap[p].length; ci++) {
if (cityMap[p][ci].id === selectedCityId) { activeProv = p; selectedCityName = cityMap[p][ci].name; break; }
}
if (activeProv) break;
}
renderProvinces(s.provinces);
if (activeProv) renderCities(activeProv);
if (activeProv && selectedCityName) {
pickerText.textContent = activeProv + ' · ' + selectedCityName;
}
}
if (s.themes && s.themes.length) {
var themeSel = document.getElementById('themeSelect');
s.themes.forEach(function(t) {
var opt = document.createElement('option');
opt.value = t.value;
opt.textContent = t.label;
themeSel.appendChild(opt);
});
}
toggleKeys.forEach(function(k) {
if (s[k] !== undefined) document.getElementById(k).checked = s[k];
});
if (s.layout) document.getElementById('layout').value = s.layout;
if (s.zodiac) document.getElementById('zodiacSelect').value = s.zodiac;
if (s.theme) document.getElementById('themeSelect').value = s.theme;
if (s.wallpaperText) document.getElementById('wallpaperText').value = s.wallpaperText;
if (s.imagePath) document.getElementById('imagePathDisplay').textContent = s.imagePath;
if (s.knowledgeKeyword) document.getElementById('knowledgeKeyword').value = s.knowledgeKeyword;
if (s.knowledgePrompt) document.getElementById('knowledgePrompt').value = s.knowledgePrompt;
if (s.theme === 'text') document.getElementById('textInputRow').style.display = 'flex';
// Color state
if (s.color1) { currentColor1 = s.color1; currentColor2 = s.color2 || ''; currentGradient = s.colorGradient || false; }
if (s.wallpaperType === 'color') updateColorPreview();
savedColors = s.savedColors || [];
renderSavedColors();
setWpType(s.wallpaperType || 'theme');
// Bing state
if (s.wallpaperType === 'bing' && window.getBingInfo) {
window.getBingInfo().then(function(si) { updateBingUI(si); });
}
if (s.wallpaperType === 'bing') loadBingFavorites();
if (s.bingAutoRefresh !== undefined) document.getElementById('bingAutoRefresh').checked = s.bingAutoRefresh;
// resize window to fit content
setTimeout(function() {
var el = document.documentElement;
var cw = el.scrollWidth;
var ch = el.scrollHeight + 8;
if (window.resizeToFit) window.resizeToFit(cw, ch);
}, 100);
initDone = true;
});
}
</script>
</body>
</html>

239
web/themes/fractal_src.html Normal file
View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Dynamic Wallpaper</title>
<style>
* { margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
#info {
position: fixed; top: 20px; right: 20px;
color: rgba(255,255,255,0.6); font: 14px monospace;
pointer-events: none; user-select: none;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="info"></div>
<script>
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) { document.body.innerHTML = '<h1 style="color:#fff;text-align:center;margin-top:40vh">WebGL 不可用</h1>'; }
let paused = false;
let fullscreen = false;
let mouseX = 0, mouseY = 0;
let lastMove = 0;
// --- 状态控制 (Go Bridge) ---
window.setPaused = function(v) { paused = v; };
window.setFullscreen = function(v) { fullscreen = v; };
// --- 鼠标 ---
document.addEventListener('mousemove', e => {
mouseX = e.clientX / window.innerWidth;
mouseY = 1.0 - e.clientY / window.innerHeight;
lastMove = performance.now();
});
document.addEventListener('click', e => {
// 点击涟漪效果 — 传给 shader
clickX = e.clientX / window.innerWidth;
clickY = 1.0 - e.clientY / window.innerHeight;
clickTime = performance.now();
});
let clickX = 0, clickY = 0, clickTime = 0;
// --- Shader ---
const vertSrc = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;
// 极光 + 流体噪声 shader
const fragSrc = `
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_click;
uniform vec2 u_clickPos;
// simplex-like noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m; m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 p = uv * 2.0 - 1.0;
p.x *= u_resolution.x / u_resolution.y;
float t = u_time * 0.3;
// 鼠标影响
vec2 mp = u_mouse * 2.0 - 1.0;
mp.x *= u_resolution.x / u_resolution.y;
float mouseDist = length(p - mp);
float mouseInfluence = smoothstep(0.8, 0.0, mouseDist) * 0.3;
// 点击涟漪
float ripple = 0.0;
if (u_click > 0.0) {
vec2 cp = u_clickPos * 2.0 - 1.0;
cp.x *= u_resolution.x / u_resolution.y;
float d = length(p - cp);
ripple = sin(d * 30.0 - u_click * 8.0) * exp(-d * 3.0) * exp(-u_click * 2.0) * 0.15;
}
// 多层噪声
float n1 = snoise(p * 1.5 + vec2(t * 0.5, t * 0.3)) * 0.5 + 0.5;
float n2 = snoise(p * 3.0 + vec2(-t * 0.7, t * 0.5)) * 0.5 + 0.5;
float n3 = snoise(p * 0.8 + vec2(t * 0.2, -t * 0.4) + mouseInfluence) * 0.5 + 0.5;
// 极光色彩
vec3 c1 = vec3(0.05, 0.2, 0.4); // 深蓝
vec3 c2 = vec3(0.0, 0.8, 0.5); // 翠绿
vec3 c3 = vec3(0.3, 0.1, 0.6); // 紫色
vec3 c4 = vec3(0.1, 0.5, 0.9); // 天蓝
// 极光带
float aurora = smoothstep(0.3, 0.7, n3) * smoothstep(0.8, 0.4, n1);
vec3 auroraColor = mix(c2, c4, n2) * aurora * 1.2;
// 底层渐变
vec3 bg = mix(c1, c3, uv.y * 0.5 + n1 * 0.3);
bg += auroraColor;
// 星星效果
float stars = pow(snoise(p * 20.0), 12.0) * 0.8;
bg += vec3(stars);
// 涟漪叠加
bg += vec3(ripple * 2.0, ripple * 3.0, ripple * 4.0);
// 鼠标光晕
bg += vec3(0.1, 0.3, 0.5) * mouseInfluence;
// 轻微暗角
float vignette = 1.0 - smoothstep(0.5, 1.5, length(p * 0.7));
bg *= vignette * 0.9 + 0.1;
gl_FragColor = vec4(bg, 1.0);
}
`;
// --- 编译 Shader ---
function createShader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error('Shader error:', gl.getShaderInfoLog(s));
return null;
}
return s;
}
const vs = createShader(gl.VERTEX_SHADER, vertSrc);
const fs = createShader(gl.FRAGMENT_SHADER, fragSrc);
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
gl.useProgram(prog);
// 全屏四边形
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// Uniforms
const uTime = gl.getUniformLocation(prog, 'u_time');
const uRes = gl.getUniformLocation(prog, 'u_resolution');
const uMouse = gl.getUniformLocation(prog, 'u_mouse');
const uClick = gl.getUniformLocation(prog, 'u_click');
const uClickPos = gl.getUniformLocation(prog, 'u_clickPos');
// --- 渲染循环 ---
let lastFrame = 0;
let targetFPS = 30;
let currentFPS = 30;
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
function render(now) {
requestAnimationFrame(render);
if (paused || fullscreen) return;
// 帧率控制
const interval = 1000 / targetFPS;
if (now - lastFrame < interval) return;
lastFrame = now;
// 自适应帧率:鼠标静止 5s 后降帧
const idleTime = now - lastMove;
if (idleTime > 5000) {
targetFPS = 10;
} else {
targetFPS = 30;
}
const t = now / 1000.0;
const clickElapsed = clickTime > 0 ? (now - clickTime) / 1000.0 : 0.0;
gl.uniform1f(uTime, t);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform2f(uMouse, mouseX, mouseY);
gl.uniform1f(uClick, clickElapsed > 3.0 ? 0.0 : clickElapsed);
gl.uniform2f(uClickPos, clickX, clickY);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
requestAnimationFrame(render);
// 通知 Go 层壁纸就绪
if (window.wallpaperReady) {
wallpaperReady().then(() => console.log('wallpaper embedded'));
}
</script>
</body>
</html>