update: 2026-03-28 20:59
This commit is contained in:
42
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/index.html
Normal file
42
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI 聊天</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="chat-container">
|
||||
<div class="warning-banner" role="alert">⚠️ 重要提示:本AI聊天功能为免费体验服务,旨在展示不同大语言模型的交互能力。请注意:1.不支持上下文记忆功能;2.每分钟调用次数有限制;3.使用人数过多会出现调用失败情况</div>
|
||||
<div class="chat-header">
|
||||
<h2>AI 聊天</h2>
|
||||
<div class="model-selector">
|
||||
<label for="model">选择模型: </label>
|
||||
<select id="model">
|
||||
<option value="openai/gpt-4.1">gpt-4.1</option>
|
||||
<option value="openai/gpt-4.1-mini">gpt-4.1-mini</option>
|
||||
<option value="openai/gpt-4.1-nano">gpt-4.1-nano</option>
|
||||
<option value="openai/gpt-4o">gpt-4o</option>
|
||||
<option value="openai/gpt-5">gpt-5</option>
|
||||
<option value="deepseek-r1">deepseek-r1</option>
|
||||
<option value="deepseek-v3-0324">deepseek-v3-0324</option>
|
||||
<option value="xai/grok-3">grok-3</option>
|
||||
<option value="xai/grok-3-mini">grok-3-mini</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="model-info" class="model-info"></div>
|
||||
<div class="chat-box" id="chat-box">
|
||||
<!-- 消息将在这里显示 -->
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<input type="text" id="user-input" placeholder="输入你的消息...">
|
||||
<button id="send-btn">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="marked.min.js"></script>
|
||||
<script src="purify.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/marked.min.js
vendored
Normal file
69
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/purify.min.js
vendored
Normal file
3
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
223
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/script.js
Normal file
223
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/script.js
Normal file
@@ -0,0 +1,223 @@
|
||||
// 简单的静态网页调用 GitHub Models API 进行聊天
|
||||
// 注意:将 token 暴露在前端存在安全风险,仅用于本地演示
|
||||
|
||||
const endpoint = "https://models.github.ai/inference";
|
||||
let apiKey = ""; // 注意:已硬编码,仅用于本地演示
|
||||
|
||||
const chatBox = document.getElementById("chat-box");
|
||||
const userInput = document.getElementById("user-input");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const modelSelect = document.getElementById("model");
|
||||
|
||||
// 模型信息映射(中文描述与上下文上限)
|
||||
const MODEL_INFO = {
|
||||
"openai/gpt-4.1": {
|
||||
name: "gpt-4.1",
|
||||
inputTokens: "1049k",
|
||||
outputTokens: "33k",
|
||||
about: "在各方面优于 gpt-4o,编码、指令跟随与长上下文理解均有显著提升"
|
||||
},
|
||||
"openai/gpt-4.1-mini": {
|
||||
name: "gpt-4.1-mini",
|
||||
inputTokens: "1049k",
|
||||
outputTokens: "33k",
|
||||
about: "在各方面优于 gpt-4o-mini,在编码、指令跟随与长上下文处理上有显著提升"
|
||||
},
|
||||
"openai/gpt-4.1-nano": {
|
||||
name: "gpt-4.1-nano",
|
||||
inputTokens: "1049k",
|
||||
outputTokens: "33k",
|
||||
about: "在编码、指令跟随与长上下文处理上有所提升,同时具备更低延迟与成本"
|
||||
},
|
||||
"openai/gpt-4o": {
|
||||
name: "gpt-4o",
|
||||
inputTokens: "131k",
|
||||
outputTokens: "16k",
|
||||
about: "OpenAI 最先进的多模态 gpt-4o 家族模型,可处理文本与图像输入"
|
||||
},
|
||||
"openai/gpt-5": {
|
||||
name: "gpt-5",
|
||||
inputTokens: "200k",
|
||||
outputTokens: "100k",
|
||||
about: "针对逻辑密集与多步骤任务设计"
|
||||
},
|
||||
"deepseek-r1": {
|
||||
name: "deepseek-r1",
|
||||
inputTokens: "128k",
|
||||
outputTokens: "4k",
|
||||
about: "通过逐步训练过程在推理任务上表现出色,适用于语言、科学推理与代码生成等"
|
||||
},
|
||||
"deepseek-v3-0324": {
|
||||
name: "deepseek-v3-0324",
|
||||
inputTokens: "128k",
|
||||
outputTokens: "4k",
|
||||
about: "相较于 DeepSeek-V3 在关键方面显著提升,包括更强的推理能力、函数调用与代码生成表现"
|
||||
},
|
||||
"xai/grok-3": {
|
||||
name: "grok-3",
|
||||
inputTokens: "131k",
|
||||
outputTokens: "4k",
|
||||
about: "Grok 3 是 xAI 的首发模型,由 Colossus 在超大规模上进行预训练,在金融、医疗和法律等专业领域表现突出。"
|
||||
},
|
||||
"xai/grok-3-mini": {
|
||||
name: "grok-3-mini",
|
||||
inputTokens: "131k",
|
||||
outputTokens: "4k",
|
||||
about: "Grok 3 Mini 是一款轻量级模型,会在答复前进行思考。它针对数学与科学问题进行训练,特别适合逻辑类任务。"
|
||||
}
|
||||
};
|
||||
|
||||
function renderModelInfo() {
|
||||
const m = MODEL_INFO[modelSelect.value];
|
||||
const infoEl = document.getElementById('model-info');
|
||||
if (!infoEl) return;
|
||||
if (m) {
|
||||
infoEl.innerHTML = `<div><span class="name">${m.name}</span> <span class="tokens">上下文 ${m.inputTokens} 输入 · ${m.outputTokens} 输出</span></div><div class="about">简介:${m.about}</div>`;
|
||||
} else {
|
||||
infoEl.innerHTML = `<div class="about">未配置该模型的上下文限制信息</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Markdown 解析配置(若库已加载)
|
||||
if (window.marked) {
|
||||
marked.setOptions({ gfm: true, breaks: true, headerIds: true, mangle: false });
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
try {
|
||||
if (window.marked && window.DOMPurify) {
|
||||
const html = marked.parse(text || '');
|
||||
return DOMPurify.sanitize(html);
|
||||
}
|
||||
} catch (_) {}
|
||||
return text || '';
|
||||
}
|
||||
|
||||
function appendMessage(role, text){
|
||||
const message = document.createElement('div');
|
||||
message.className = `message ${role}`;
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble';
|
||||
// Markdown 渲染
|
||||
bubble.innerHTML = renderMarkdown(text);
|
||||
message.appendChild(bubble);
|
||||
chatBox.appendChild(message);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
|
||||
function appendStreamingMessage(role){
|
||||
const message = document.createElement('div');
|
||||
message.className = `message ${role}`;
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble';
|
||||
bubble._mdBuffer = '';
|
||||
bubble.innerHTML = '';
|
||||
message.appendChild(bubble);
|
||||
chatBox.appendChild(message);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
return bubble;
|
||||
}
|
||||
|
||||
async function sendMessage(){
|
||||
const content = userInput.value.trim();
|
||||
if (!content) return;
|
||||
appendMessage('user', content);
|
||||
userInput.value = '';
|
||||
|
||||
const model = modelSelect.value;
|
||||
|
||||
// 令牌已硬编码(本地演示),如未配置则提示
|
||||
if (!apiKey) {
|
||||
appendMessage('assistant', '未配置令牌');
|
||||
return;
|
||||
}
|
||||
|
||||
sendBtn.disabled = true;
|
||||
const assistantBubble = appendStreamingMessage('assistant');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${endpoint}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
temperature: 1,
|
||||
top_p: 1,
|
||||
stream: true,
|
||||
messages: [
|
||||
{ role: 'system', content: '' },
|
||||
{ role: 'user', content }
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
throw new Error(`HTTP ${res.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let doneStream = false;
|
||||
|
||||
while (!doneStream) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const parts = buffer.split(/\r?\n/);
|
||||
buffer = parts.pop();
|
||||
for (const part of parts) {
|
||||
const line = part.trim();
|
||||
if (line === '') continue;
|
||||
if (line.startsWith('data:')) {
|
||||
const payload = line.slice(5).trim();
|
||||
if (payload === '[DONE]') {
|
||||
doneStream = true;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(payload);
|
||||
const delta = json?.choices?.[0]?.delta?.content ?? json?.choices?.[0]?.message?.content ?? '';
|
||||
if (delta) {
|
||||
// 累计流式文本并增量渲染 Markdown
|
||||
assistantBubble._mdBuffer = (assistantBubble._mdBuffer || '') + delta;
|
||||
const safeHtml = renderMarkdown(assistantBubble._mdBuffer);
|
||||
assistantBubble.innerHTML = safeHtml;
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略无法解析的行
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!assistantBubble._mdBuffer || !assistantBubble.textContent) {
|
||||
assistantBubble.textContent = '(无内容返回)';
|
||||
}
|
||||
} catch (err) {
|
||||
//assistantBubble.textContent = `出错了:${err.message}`;
|
||||
assistantBubble.textContent = `调用次数过多或者使用人数过多,请稍后再试!`;
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
userInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 切换模型时更新信息面板,初次渲染一次
|
||||
modelSelect.addEventListener('change', renderModelInfo);
|
||||
renderModelInfo();
|
||||
172
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/style.css
Normal file
172
InfoGenie-frontend/public/toolbox/网页小玩具/AI聊天/style.css
Normal file
@@ -0,0 +1,172 @@
|
||||
/* 淡绿色淡黄绿色渐变清新柔和风格,移动端适配 */
|
||||
:root {
|
||||
--bg-start: #d9f7be; /* 淡绿 */
|
||||
--bg-end: #f4f9d2; /* 淡黄绿 */
|
||||
--primary: #4caf50; /* 绿色强调 */
|
||||
--secondary: #8bc34a;
|
||||
--text: #2f3b2f;
|
||||
--muted: #6b7a6b;
|
||||
--white: #ffffff;
|
||||
--shadow: rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部警示通知样式 */
|
||||
.warning-banner {
|
||||
padding: 10px 16px;
|
||||
background: #fff8d6; /* 柔和黄色背景 */
|
||||
border: 1px solid #ffec99; /* 黄色边框 */
|
||||
border-left: 4px solid #faad14; /* 左侧强调条 */
|
||||
color: #5c4a00; /* 深色文字保证可读性 */
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.warning-banner { font-size: 13px; padding: 8px 12px; }
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #eafbe6, #f9ffe6);
|
||||
border-bottom: 1px solid #e1f3d8;
|
||||
}
|
||||
|
||||
.chat-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.model-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-selector select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #cfe8c9;
|
||||
border-radius: 8px;
|
||||
background: #f7fff2;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
height: 60vh;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background: #fbfff5;
|
||||
}
|
||||
|
||||
.message { display: flex; margin-bottom: 12px; gap: 8px; }
|
||||
.message .bubble {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
max-width: 85%;
|
||||
box-shadow: 0 2px 10px var(--shadow);
|
||||
}
|
||||
.message.user .bubble {
|
||||
background: #e2f7d8;
|
||||
align-self: flex-end;
|
||||
}
|
||||
.message.assistant .bubble {
|
||||
background: #fffef0;
|
||||
}
|
||||
.message.user { justify-content: flex-end; }
|
||||
.message.assistant { justify-content: flex-start; }
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #e1f3d8;
|
||||
background: #fafff0;
|
||||
}
|
||||
|
||||
#user-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #cfe8c9;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
#send-btn {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--secondary), var(--primary));
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
#send-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
/* 模型信息面板样式 */
|
||||
.model-info {
|
||||
padding: 10px 16px;
|
||||
background: #f9fff0;
|
||||
border-bottom: 1px solid #e1f3d8;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.model-info .name { color: var(--primary); font-weight: 600; }
|
||||
.model-info .tokens { color: var(--text); font-weight: 600; margin-left: 8px; }
|
||||
.model-info .about { margin-top: 6px; line-height: 1.5; }
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 480px) {
|
||||
.chat-box { height: 62vh; }
|
||||
#user-input { font-size: 15px; }
|
||||
#send-btn { padding: 10px 14px; }
|
||||
.model-info { font-size: 13px; padding: 8px 12px; }
|
||||
}
|
||||
|
||||
/* 全局隐藏滚动条,保留滚动效果 */
|
||||
html, body {
|
||||
-ms-overflow-style: none; /* IE/旧版 Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
html::-webkit-scrollbar,
|
||||
body::-webkit-scrollbar { display: none; } /* Chrome/Safari/新 Edge */
|
||||
|
||||
/* 隐藏所有元素滚动条(覆盖常见浏览器) */
|
||||
* {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
*::-webkit-scrollbar { display: none; }
|
||||
|
||||
/* 保持聊天框可滚动并优化移动端滚动体验 */
|
||||
.chat-box { -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* 代码块允许横向滚动但隐藏滚动条 */
|
||||
.message .bubble pre { overflow-x: auto; }
|
||||
.message .bubble pre { scrollbar-width: none; -ms-overflow-style: none; }
|
||||
.message .bubble pre::-webkit-scrollbar { display: none; }
|
||||
202
InfoGenie-frontend/public/toolbox/网页小玩具/人生倒计时/index.html
Normal file
202
InfoGenie-frontend/public/toolbox/网页小玩具/人生倒计时/index.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html><html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>人生倒计时</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg-start:#e8f8e4; /* 淡绿 */
|
||||
--bg-end:#f5fbdc; /* 淡黄绿 */
|
||||
--text:#1f3830;
|
||||
--muted:#5c7a68;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0; min-height:100svh; color:var(--text);
|
||||
font-family: system-ui,-apple-system,"PingFang SC","Microsoft YaHei",Segoe UI,Roboto,Helvetica,Arial,"Noto Sans CJK SC","Noto Sans",sans-serif;
|
||||
background: linear-gradient(135deg,var(--bg-start),var(--bg-end));
|
||||
background-size:200% 200%;
|
||||
animation: flow 12s ease-in-out infinite;
|
||||
}
|
||||
@keyframes flow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}.container{max-width:680px;margin:0 auto;padding:24px 16px 80px} .header{text-align:center;margin-bottom:16px} .title{font-size:clamp(28px,7vw,40px);font-weight:900;letter-spacing:2px; background:linear-gradient(90deg,#4caf50,#8bc34a,#aed581);-webkit-background-clip:text;color:transparent; filter:drop-shadow(0 2px 6px rgba(76,175,80,.18));} .subtitle{color:var(--muted);font-size:clamp(13px,3.5vw,15px)} .grid{display:grid;gap:14px;grid-template-columns:1fr} .card{position:relative;padding:16px 14px;border-radius:20px;background:rgba(255,255,255,.55); box-shadow:0 10px 25px rgba(93,125,106,.18), inset 0 1px 0 rgba(255,255,255,.6); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,.6); } .card::after{content:""; position:absolute; inset:-2px; border-radius:22px; pointer-events:none; background:linear-gradient(120deg, rgba(124,179,66,.35), rgba(255,255,255,0) 40%, rgba(85,204,170,.3)); mask: linear-gradient(#000,#000) exclude, linear-gradient(#000 0 0); } .section-title{display:flex;align-items:center;gap:8px;font-weight:800;font-size:16px;letter-spacing:.2px} .kbd{font-size:11px;color:var(--muted);border:1px solid rgba(0,0,0,.05);padding:2px 6px;border-radius:8px;background:rgba(255,255,255,.7)} .row{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-top:10px;flex-wrap:wrap} .metric{font-size:14px;color:var(--muted)} .value{font-feature-settings:"tnum" 1,"lnum" 1;font-variant-numeric:tabular-nums;font-weight:800;font-size:clamp(18px,5.5vw,24px)} .progress{width:100%;height:14px;background:rgba(0,0,0,.06);border-radius:999px;overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,.12)} .bar{height:100%;width:0%;border-radius:inherit;transition:width .8s cubic-bezier(.22,.61,.36,1);position:relative} .bar::after{content:"";position:absolute;inset:0;background:linear-gradient(to right,rgba(255,255,255,0) 0%,rgba(255,255,255,.35) 30%,rgba(255,255,255,0) 60%);transform:translateX(-100%);animation:shimmer 2.4s infinite} @keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}} .bar.c1{background:linear-gradient(90deg,#7bc67b,#cde77f)} .bar.c2{background:linear-gradient(90deg,#77d0b5,#b5e48c)} .bar.c3{background:linear-gradient(90deg,#bade6d,#e6f39a)} .bar.c4{background:linear-gradient(90deg,#6ac26a,#a3e07a)} .badge{display:inline-flex;align-items:center;gap:6px;font-size:13px;padding:8px 10px;border-radius:999px;background:rgba(255,255,255,.6);border:1px solid rgba(0,0,0,.06);box-shadow:0 4px 12px rgba(0,0,0,.06)} .badge b{font-feature-settings:"tnum" 1,"lnum" 1;font-variant-numeric:tabular-nums} .holidays{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-top:12px} .holiday{padding:12px;border-radius:18px;background:linear-gradient(135deg,rgba(139,195,74,.25),rgba(205,220,57,.2));border:1px dashed rgba(139,195,74,.35);box-shadow:inset 0 1px 0 rgba(255,255,255,.5)} .holiday h4{margin:0 0 8px;font-size:14px} .holiday .days{font-weight:900;font-size:24px} .footer{text-align:center;margin-top:22px;color:var(--muted);font-size:12px} .small{font-size:12px;color:var(--muted)}
|
||||
|
||||
/* 轻微装饰粒子效果 */ .particles{position:fixed;inset:0;overflow:hidden;pointer-events:none} .particles span{position:absolute;width:clamp(6px,1.8vw,10px);height:clamp(6px,1.8vw,10px); background:radial-gradient(circle at 30% 30%,rgba(255,255,255,.9),rgba(255,255,255,.2));border-radius:50%;filter:blur(.2px); animation:float 14s linear infinite;opacity:.6} @keyframes float{0%{transform:translateY(110vh) translateX(0) scale(1) rotate(0)}100%{transform:translateY(-10vh) translateX(40px) scale(1.4) rotate(180deg)}} .particles span:nth-child(odd){animation-duration:18s} .particles span:nth-child(3n){animation-direction:reverse} .particles span:nth-child(4n){animation-duration:22s}
|
||||
|
||||
@media (min-width:760px){.grid{grid-template-columns:1fr 1fr}.card{padding:18px 16px}} </style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="particles" aria-hidden="true"></div>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">人生倒计时</h1>
|
||||
</header> <main class="grid">
|
||||
<section class="card" id="today">
|
||||
<div class="section-title">今天 · <span class="kbd">剩余小时</span></div>
|
||||
<div class="row">
|
||||
<div class="metric">距离今天结束还有</div>
|
||||
<div class="value"><span id="todayHours">--</span> 小时 <span class="small" id="todayHMS"></span></div>
|
||||
</div>
|
||||
<div class="progress" aria-label="今日剩余百分比">
|
||||
<div class="bar c1" id="barToday"></div>
|
||||
</div>
|
||||
</section><section class="card" id="week">
|
||||
<div class="section-title">本周 · <span class="kbd">剩余天数</span></div>
|
||||
<div class="row">
|
||||
<div class="metric">(周一为一周起点)</div>
|
||||
<div class="value"><span id="weekDays">--</span> 天</div>
|
||||
</div>
|
||||
<div class="progress" aria-label="本周剩余百分比">
|
||||
<div class="bar c2" id="barWeek"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="month">
|
||||
<div class="section-title">本月 · <span class="kbd">剩余天数</span></div>
|
||||
<div class="row">
|
||||
<div class="metric">当前月份还剩</div>
|
||||
<div class="value"><span id="monthDays">--</span> 天</div>
|
||||
</div>
|
||||
<div class="progress" aria-label="本月剩余百分比">
|
||||
<div class="bar c3" id="barMonth"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="year">
|
||||
<div class="section-title">2025 · <span class="kbd">剩余天数</span></div>
|
||||
<div class="row">
|
||||
<div class="metric">距离今年结束还有</div>
|
||||
<div class="value"><span id="yearDays">--</span> 天</div>
|
||||
</div>
|
||||
<div class="progress" aria-label="本年剩余百分比">
|
||||
<div class="bar c4" id="barYear"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="holidays">
|
||||
<div class="section-title">重要节日倒计时</div>
|
||||
<div class="holidays">
|
||||
<div class="holiday">
|
||||
<h4>五一劳动节</h4>
|
||||
<div class="days"><span id="d51">--</span> 天</div>
|
||||
<div class="small" id="d51Date"></div>
|
||||
</div>
|
||||
<div class="holiday">
|
||||
<h4>国庆节</h4>
|
||||
<div class="days"><span id="dGQ">--</span> 天</div>
|
||||
<div class="small" id="dGQDate"></div>
|
||||
</div>
|
||||
<div class="holiday">
|
||||
<h4>圣诞节</h4>
|
||||
<div class="days"><span id="dXmas">--</span> 天</div>
|
||||
<div class="small" id="dXmasDate"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div><script>
|
||||
(function(){
|
||||
// ===== 常用工具函数 =====
|
||||
const pad = n => String(n).padStart(2,'0');
|
||||
const isLeapYear = y => (y%4===0 && y%100!==0) || (y%400===0);
|
||||
const daysInYear = y => isLeapYear(y) ? 366 : 365;
|
||||
const daysInMonth = (y,m)=> new Date(y, m+1, 0).getDate(); // m:0-11
|
||||
|
||||
// 结束时间(以下一单位0点为界,方便计算剩余比例)
|
||||
const endOfToday = now => new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);
|
||||
// 周一为一周的起点,周末结束到下周一0点
|
||||
function endOfWeek(now){
|
||||
const day = (now.getDay()+6)%7; // 0..6 (Mon..Sun)
|
||||
const daysToNextMon = 7 - day;
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate()+daysToNextMon);
|
||||
}
|
||||
const endOfMonth = now => new Date(now.getFullYear(), now.getMonth()+1, 1);
|
||||
const endOfYear = now => new Date(now.getFullYear()+1, 0, 1);
|
||||
|
||||
const msToHMS = ms => {
|
||||
const sec = Math.max(0, Math.floor(ms/1000));
|
||||
const s = sec % 60; const m = Math.floor(sec/60)%60; const h = Math.floor(sec/3600);
|
||||
return h+":"+pad(m)+":"+pad(s);
|
||||
}
|
||||
const niceDays = ms => Math.max(0, (ms/86400000));
|
||||
const setBar = (id, percent) => { document.getElementById(id).style.width = Math.max(0,Math.min(100,percent)).toFixed(2)+"%"; };
|
||||
|
||||
function update(){
|
||||
const now = new Date();
|
||||
|
||||
// 今天剩余小时
|
||||
const eToday = endOfToday(now);
|
||||
const msLeftToday = eToday - now;
|
||||
const hoursLeft = msLeftToday / 3600000;
|
||||
document.getElementById("todayHours").textContent = hoursLeft.toFixed(2);
|
||||
document.getElementById("todayHMS").textContent = "(约 "+ msToHMS(msLeftToday) +")";
|
||||
setBar("barToday", (hoursLeft/24)*100);
|
||||
|
||||
// 本周剩余天数(周一为起点)
|
||||
const eWeek = endOfWeek(now);
|
||||
const msLeftWeek = eWeek - now;
|
||||
const daysLeftWeek = niceDays(msLeftWeek);
|
||||
document.getElementById("weekDays").textContent = daysLeftWeek.toFixed(2);
|
||||
setBar("barWeek", (daysLeftWeek/7)*100);
|
||||
|
||||
// 本月剩余天数
|
||||
const eMonth = endOfMonth(now);
|
||||
const msLeftMonth = eMonth - now;
|
||||
const dInMonth = daysInMonth(now.getFullYear(), now.getMonth());
|
||||
const daysLeftMonth = niceDays(msLeftMonth);
|
||||
document.getElementById("monthDays").textContent = daysLeftMonth.toFixed(2);
|
||||
setBar("barMonth", (daysLeftMonth/dInMonth)*100);
|
||||
|
||||
// 今年剩余天数(以 2025 年为展示基准)
|
||||
const eYear = endOfYear(now);
|
||||
const msLeftYear = eYear - now;
|
||||
const dInYear = daysInYear(now.getFullYear());
|
||||
const daysLeftYear = niceDays(msLeftYear);
|
||||
document.getElementById("yearDays").textContent = daysLeftYear.toFixed(2);
|
||||
setBar("barYear", (daysLeftYear/dInYear)*100);
|
||||
|
||||
// 重要节日
|
||||
const thisYear = now.getFullYear();
|
||||
function nextDate(mm, dd){
|
||||
let d = new Date(thisYear, mm-1, dd);
|
||||
if (now >= endOfToday(d)) d.setFullYear(thisYear+1);
|
||||
return d;
|
||||
}
|
||||
function daysUntil(target){
|
||||
// 以当天0点为日界,向上取整到“天”
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
return Math.ceil((target - start)/86400000);
|
||||
}
|
||||
const d51 = nextDate(5,1);
|
||||
const dGQ = nextDate(10,1);
|
||||
const dXmas = nextDate(12,25);
|
||||
document.getElementById("d51").textContent = daysUntil(d51);
|
||||
document.getElementById("dGQ").textContent = daysUntil(dGQ);
|
||||
document.getElementById("dXmas").textContent = daysUntil(dXmas);
|
||||
document.getElementById("d51Date").textContent = d51.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric',weekday:'short'});
|
||||
document.getElementById("dGQDate").textContent = dGQ.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric',weekday:'short'});
|
||||
document.getElementById("dXmasDate").textContent = dXmas.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric',weekday:'short'});
|
||||
|
||||
// 底部时间
|
||||
const tn = now.toLocaleString(undefined,{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||||
}
|
||||
|
||||
// 装饰粒子生成
|
||||
(function spawnParticles(){
|
||||
const wrap = document.querySelector('.particles');
|
||||
for(let i=0;i<28;i++){
|
||||
const s = document.createElement('span');
|
||||
s.style.left = Math.random()*100 + 'vw';
|
||||
s.style.animationDelay = (-Math.random()*20) + 's';
|
||||
s.style.opacity = (0.35 + Math.random()*0.4).toFixed(2);
|
||||
wrap.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
update();
|
||||
setInterval(update, 1000);
|
||||
})();
|
||||
</script></body>
|
||||
</html>
|
||||
113
InfoGenie-frontend/public/toolbox/网页小玩具/人生倒计时/人生倒计时.html
Normal file
113
InfoGenie-frontend/public/toolbox/网页小玩具/人生倒计时/人生倒计时.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<div id="life-countdown" style="border: 1px solid #ddd; padding: 10px; border-radius: 5px;">
|
||||
<h3 class="colorful-text" style="text-align: center;">人生倒计时</h3>
|
||||
<div class="countdown-section" style="margin: 10px 0;">
|
||||
<div class="countdown-text" style="color: red;">今天已过去 <span id="hours-today"></span> 小时</div>
|
||||
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
|
||||
<div id="bar-today" class="progress" style="background: #f39c12;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown-section" style="margin: 10px 0;">
|
||||
<div class="countdown-text" style="color: orange;">本周已过去 <span id="days-week"></span> 天</div>
|
||||
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
|
||||
<div id="bar-week" class="progress" style="background: #3498db;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown-section" style="margin: 10px 0;">
|
||||
<div class="countdown-text" style="color: yellow;">本月已过去 <span id="days-month"></span> 天</div>
|
||||
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
|
||||
<div id="bar-month" class="progress" style="background: #2ecc71;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown-section" style="margin: 10px 0;">
|
||||
<div class="countdown-text" style="color: green;">今年已过去 <span id="days-year"></span> 天</div>
|
||||
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
|
||||
<div id="bar-year" class="progress" style="background: #9b59b6;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown-section" style="margin: 10px 0;">
|
||||
<div class="countdown-text" style="color: blue;">距离春节还有 <span id="days-chunjie"></span> 天</div>
|
||||
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
|
||||
<div id="bar-chunjie" class="progress" style="background: #2980b9;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown-section" style="margin: 10px 0;">
|
||||
<div class="countdown-text" style="color: purple;">距离我的生日还有 <span id="days-birthday"></span> 天</div>
|
||||
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
|
||||
<div id="bar-birthday" class="progress" style="background: #e74c3c;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.colorful-text {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #ff6347; /* 可以根据需要调整颜色 */
|
||||
}
|
||||
|
||||
.countdown-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 20px;
|
||||
transition: width 1s ease-in-out;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function updateCountdown() {
|
||||
const now = new Date();
|
||||
|
||||
// 今天已过去的小时数
|
||||
const hoursToday = now.getHours();
|
||||
document.getElementById('hours-today').innerText = hoursToday;
|
||||
document.getElementById('bar-today').style.width = (hoursToday / 24 * 100) + '%';
|
||||
|
||||
// 本周已过去的天数
|
||||
const daysWeek = now.getDay();
|
||||
document.getElementById('days-week').innerText = daysWeek;
|
||||
document.getElementById('bar-week').style.width = (daysWeek / 7 * 100) + '%';
|
||||
|
||||
// 本月已过去的天数
|
||||
const daysMonth = now.getDate();
|
||||
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
||||
document.getElementById('days-month').innerText = daysMonth;
|
||||
document.getElementById('bar-month').style.width = (daysMonth / daysInMonth * 100) + '%';
|
||||
|
||||
// 今年已过去的天数
|
||||
const start = new Date(now.getFullYear(), 0, 0);
|
||||
const diff = now - start;
|
||||
const oneDay = 1000 * 60 * 60 * 24;
|
||||
const daysYear = Math.floor(diff / oneDay);
|
||||
document.getElementById('days-year').innerText = daysYear;
|
||||
document.getElementById('bar-year').style.width = (daysYear / 365 * 100) + '%';
|
||||
|
||||
// 离春节还有多少天(2025年1月29日)
|
||||
const chunjieDate = new Date(2025, 0, 29); // 春节日期假设为2025年1月29日
|
||||
const diffChunjie = chunjieDate - now;
|
||||
const daysChunjie = Math.floor(diffChunjie / oneDay);
|
||||
document.getElementById('days-chunjie').innerText = daysChunjie;
|
||||
document.getElementById('bar-chunjie').style.width = ((365 - daysChunjie) / 365 * 100) + '%';
|
||||
|
||||
// 离生日还有多少天(每年的10月25日)
|
||||
let birthdayDate = new Date(now.getFullYear(), 9, 25);
|
||||
if (now > birthdayDate) {
|
||||
birthdayDate = new Date(now.getFullYear() + 1, 9, 25);
|
||||
}
|
||||
const diffBirthday = birthdayDate - now;
|
||||
const daysBirthday = Math.floor(diffBirthday / (1000 * 60 * 60 * 24));
|
||||
document.getElementById('days-birthday').innerText = daysBirthday;
|
||||
document.getElementById('bar-birthday').style.width = ((365 - daysBirthday) / 365 * 100) + '%';
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 3600000); // 每小时更新一次
|
||||
</script>
|
||||
51
InfoGenie-frontend/public/toolbox/网页小玩具/代码雨/index.html
Normal file
51
InfoGenie-frontend/public/toolbox/网页小玩具/代码雨/index.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>飞雪前端艺术</title>
|
||||
<style>
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
// 获取 canvas 元素的宽度和高度,并将宽度和高度设置为屏幕的可用宽度和高度。
|
||||
const width = document.getElementById("canvas").width = screen.availWidth;
|
||||
const height = document.getElementById("canvas").height = screen.availHeight;
|
||||
// 获取 canvas 的绘图上下文
|
||||
const ctx = document.getElementById("canvas").getContext("2d");
|
||||
// 创建一个大小为 width/10 的数组并填充为 0
|
||||
const arr = Array(Math.ceil(width / 10)).fill(0);
|
||||
// 创建一个字符串数组,用于存储字符。
|
||||
const str = "✧︎︎︎︎︎︎♾♲✰︎✦︎☭︎︎︎︎✵︎︎⚘︎✞︎♘♞☆︎★☼︎☾◎☽︎Ω℞№︎❂❁︎✣✶✺✷◦◉⦿☒✗☐☞◇☛︎︎⌘✘︎".split("");
|
||||
ctx.font = "10px 优设标题黑";
|
||||
function rain() {
|
||||
// 设置颜色,并绘制一个全屏的矩形
|
||||
ctx.fillStyle = "rgba(0,0,20,0.05)";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
// 设置文字的颜色
|
||||
ctx.fillStyle = '#00c8aa';
|
||||
arr.forEach(function (value, index) {
|
||||
// 根据数组的索引值来绘制文字,x 坐标为索引值 * 10,y 坐标为 value + 10。
|
||||
ctx.fillText(str[Math.floor(Math.random() * str.length)], index * 10, value + 10);
|
||||
|
||||
// 从上一次绘制的位置开始,将数组值设置为下一次绘制位置。
|
||||
arr[index] = value >= height || value > 8888 * Math.random() ? 0 : value + 10;
|
||||
});
|
||||
}
|
||||
// 每 30 毫秒执行一次 rain() 函数。
|
||||
setInterval(rain, 30);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
92
InfoGenie-frontend/public/toolbox/网页小玩具/数字时钟/index.html
Normal file
92
InfoGenie-frontend/public/toolbox/网页小玩具/数字时钟/index.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>数字时钟</title>
|
||||
<style>
|
||||
/* 基础:黑底白字、全屏居中 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
overscroll-behavior: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.stage {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: env(safe-area-inset);
|
||||
}
|
||||
/* 大号等宽数字,保证各位宽度一致,避免跳动 */
|
||||
.clock {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-variant-numeric: tabular-nums lining-nums;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* 自适应字号:在不同屏幕上都足够大 */
|
||||
font-size: clamp(12vmin, 18vmin, 22vmin);
|
||||
/* 防止 iOS 浏览器工具栏出现时的轻微抖动 */
|
||||
max-width: 100vw;
|
||||
}
|
||||
/* 横屏:按宽度再放大一些(更横向铺满) */
|
||||
@media (orientation: landscape) {
|
||||
.clock { font-size: min(20vmin, 22vw); }
|
||||
}
|
||||
/* 竖屏:把数字整体顺时针旋转 90°,看起来依旧是“横屏”的排布 */
|
||||
@media (orientation: portrait) {
|
||||
.clock { transform: rotate(90deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div id="clock" class="clock" aria-live="polite" aria-label="当前时间">00:00:00</div>
|
||||
</div> <script>
|
||||
(function() {
|
||||
const el = document.getElementById('clock');
|
||||
|
||||
function two(n) { return (n < 10 ? '0' : '') + n; }
|
||||
|
||||
function renderNow() {
|
||||
const d = new Date();
|
||||
el.textContent = `${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
// 与下一整秒对齐,尽量避免定时器漂移
|
||||
function startAlignedClock() {
|
||||
renderNow();
|
||||
const drift = 1000 - (Date.now() % 1000);
|
||||
setTimeout(function tick() {
|
||||
renderNow();
|
||||
setTimeout(tick, 1000);
|
||||
}, drift);
|
||||
}
|
||||
|
||||
// 处理移动端 100vh 问题:设置 CSS 变量供需要时使用(当前样式未直接用到,但保留以便扩展)
|
||||
function setViewportVars() {
|
||||
document.documentElement.style.setProperty('--dvw', window.innerWidth + 'px');
|
||||
document.documentElement.style.setProperty('--dvh', window.innerHeight + 'px');
|
||||
}
|
||||
|
||||
setViewportVars();
|
||||
window.addEventListener('resize', setViewportVars);
|
||||
window.addEventListener('orientationchange', setViewportVars);
|
||||
|
||||
startAlignedClock();
|
||||
})();
|
||||
</script></body>
|
||||
</html>
|
||||
509
InfoGenie-frontend/public/toolbox/网页小玩具/粒子圣诞树/粒子圣诞树.html
Normal file
509
InfoGenie-frontend/public/toolbox/网页小玩具/粒子圣诞树/粒子圣诞树.html
Normal file
@@ -0,0 +1,509 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||
<title>圣诞快乐</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; overflow: hidden; background: #000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, PingFangSC, "Microsoft YaHei", sans-serif; }
|
||||
#app { position: relative; width: 100vw; height: 100vh; }
|
||||
#canvas-container { position: absolute; inset: 0; }
|
||||
|
||||
.barrage { position: fixed; inset: 0; pointer-events: none; z-index: 9; }
|
||||
.barrage-item { position: fixed; left: 100vw; white-space: nowrap; font-weight: 700; text-shadow: 0 0 8px rgba(255,255,255,0.35), 0 0 16px rgba(255,255,255,0.2); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35)); animation-name: fly; animation-timing-function: linear; animation-fill-mode: forwards; }
|
||||
@keyframes fly { 0% { transform: translateX(0); } 100% { transform: translateX(-120vw); } }
|
||||
.fullscreen-btn { position: fixed; right: 14px; bottom: 14px; z-index: 11; padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.12); color: #fff; font-weight: 600; letter-spacing: 0.5px; backdrop-filter: blur(4px); cursor: pointer; }
|
||||
.fullscreen-btn:hover { background: rgba(255,255,255,0.18); }
|
||||
@media (max-width: 768px) { .fullscreen-btn { padding: 9px 12px; right: 10px; bottom: 10px; font-size: 13px; } }
|
||||
@media (max-width: 768px) {
|
||||
.barrage-item { font-size: clamp(14px, 3.5vw, 20px); }
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.barrage-item { font-size: clamp(16px, 2.1vw, 28px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
import { createApp, ref, onMounted, h } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
|
||||
import * as THREE from 'https://unpkg.com/three@0.164.1/build/three.module.js';
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
const isFs = ref(false);
|
||||
const barrageItems = ref([]);
|
||||
const containerEl = ref(null);
|
||||
|
||||
let renderer, scene, camera, group;
|
||||
let geoLeaves, matLeaves, pointsLeaves, posLeaves, colLeaves, velLeaves, tgtLeaves, startMorphLeaves;
|
||||
let geoOrnaments, matOrnaments, pointsOrnaments, posOrnaments, colOrnaments, velOrnaments, tgtOrnaments, startMorphOrnaments;
|
||||
let geoTrunk, matTrunk, pointsTrunk, posTrunk, colTrunk, velTrunk, tgtTrunk, startMorphTrunk;
|
||||
let geoStar, matStar, pointsStar, posStar, colStar, velStar, tgtStar, startMorphStar;
|
||||
let geoAccents, matAccents, pointsAccents, posAccents, colAccents, velAccents, tgtAccents, startMorphAccents;
|
||||
let snowGroup, snowSprites, snowVel, snowRot, snowScale, snowOpacityPhase;
|
||||
let startTime = 0;
|
||||
const disperseDuration = 3500;
|
||||
const morphDuration = 3000;
|
||||
const particleCountLeaves = 5400;
|
||||
const particleCountOrnaments = 400;
|
||||
const particleCountTrunk = 1200;
|
||||
const particleCountStar = 320;
|
||||
const particleCountAccents = 160;
|
||||
const snowCount = 200;
|
||||
const treeHeight = 14;
|
||||
const baseRadius = 6.2;
|
||||
let running = true;
|
||||
const topY = treeHeight + 1.2;
|
||||
const bottomY = -1.4;
|
||||
|
||||
const messages = [
|
||||
'圣诞快乐', '平安喜乐', '万事胜意', '心想事成', '前程似锦', '阖家幸福',
|
||||
'福星高照', '岁岁常欢愉', '诸事顺遂', '新年好运常在', '健康平安', '幸福常伴'
|
||||
];
|
||||
|
||||
function randomHsl() {
|
||||
const h = Math.floor(Math.random() * 360);
|
||||
const s = 68 + Math.random() * 22;
|
||||
const l = 50 + Math.random() * 10;
|
||||
return `hsl(${h}deg, ${s}%, ${l}%)`;
|
||||
}
|
||||
|
||||
function createSnowflakeTexture() {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = 128; c.height = 128;
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.clearRect(0, 0, 128, 128);
|
||||
ctx.translate(64, 64);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.95)';
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const ang = (Math.PI * 2 / 6) * i;
|
||||
const x = Math.cos(ang) * 48;
|
||||
const y = Math.sin(ang) * 48;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
for (let b = 1; b <= 3; b++) {
|
||||
const t = b / 4;
|
||||
const bx = Math.cos(ang) * 48 * t;
|
||||
const by = Math.sin(ang) * 48 * t;
|
||||
const sideAng = ang + Math.PI / 2;
|
||||
const len = 10 * (1 - t);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx, by);
|
||||
ctx.lineTo(bx + Math.cos(sideAng) * len, by + Math.sin(sideAng) * len);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx, by);
|
||||
ctx.lineTo(bx - Math.cos(sideAng) * len, by - Math.sin(sideAng) * len);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
const tex = new THREE.CanvasTexture(c);
|
||||
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
tex.magFilter = THREE.LinearFilter;
|
||||
tex.needsUpdate = true;
|
||||
return tex;
|
||||
}
|
||||
|
||||
function createSnowGlowTexture() {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = 128; c.height = 128;
|
||||
const ctx = c.getContext('2d');
|
||||
const grd = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
|
||||
grd.addColorStop(0, 'rgba(255,255,255,0.57)');
|
||||
grd.addColorStop(0.4, 'rgba(255,255,255,0.23)');
|
||||
grd.addColorStop(1, 'rgba(255,255,255,0.0)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fillRect(0, 0, 128, 128);
|
||||
const tex = new THREE.CanvasTexture(c);
|
||||
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
tex.magFilter = THREE.LinearFilter;
|
||||
tex.needsUpdate = true;
|
||||
return tex;
|
||||
}
|
||||
|
||||
function spawnBarrage() {
|
||||
const count = 2 + Math.floor(Math.random() * 3);
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = messages[Math.floor(Math.random() * messages.length)];
|
||||
const top = Math.random() * 90 + 5;
|
||||
const duration = 10 + Math.random() * 8;
|
||||
const color = randomHsl();
|
||||
const id = `${now}-${i}-${Math.random().toString(36).slice(2,7)}`;
|
||||
barrageItems.value.push({ id, text, top: `${top}%`, duration: `${duration}s`, color, shadow: color });
|
||||
setTimeout(() => {
|
||||
const idx = barrageItems.value.findIndex(x => x.id === id);
|
||||
if (idx >= 0) barrageItems.value.splice(idx, 1);
|
||||
}, duration * 1000 + 400);
|
||||
}
|
||||
}
|
||||
|
||||
function enterFs() {
|
||||
const el = document.getElementById('app') || containerEl.value || document.documentElement;
|
||||
const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
|
||||
if (rfs) rfs.call(el);
|
||||
}
|
||||
|
||||
function exitFs() {
|
||||
const efs = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;
|
||||
if (efs) efs.call(document);
|
||||
}
|
||||
|
||||
function toggleFs() { if (document.fullscreenElement) exitFs(); else enterFs(); }
|
||||
|
||||
function computeLeafTargets(count) {
|
||||
const arr = new Float32Array(count * 3);
|
||||
const layers = 12;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const layer = Math.floor(Math.random() * layers);
|
||||
const yBase = (layer / (layers - 1)) * treeHeight;
|
||||
const y = yBase + (Math.random() - 0.5) * (treeHeight * 0.035 + (1 - layer / layers) * 0.12);
|
||||
const t = y / treeHeight;
|
||||
const shelf = 0.45 * Math.sin(3.5 * (1 - t));
|
||||
const wobble = 0.25 * Math.sin(8 * t + Math.random() * 0.8);
|
||||
const r = Math.max(0.04, (baseRadius * (1 - t)) + shelf + wobble);
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
const x = r * Math.cos(a);
|
||||
const z = r * Math.sin(a);
|
||||
arr[i * 3] = x;
|
||||
arr[i * 3 + 1] = y;
|
||||
arr[i * 3 + 2] = z;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function computeTrunkTargets(count) {
|
||||
const arr = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ty = -1 + Math.random() * (treeHeight * 0.22);
|
||||
const tr = 0.35 + Math.random() * 0.22;
|
||||
const ta = Math.random() * Math.PI * 2;
|
||||
arr[i * 3] = tr * Math.cos(ta);
|
||||
arr[i * 3 + 1] = ty;
|
||||
arr[i * 3 + 2] = tr * Math.sin(ta);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function computeOrnamentTargets(count) {
|
||||
const arr = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const y = Math.random() * treeHeight * 0.95;
|
||||
const t = y / treeHeight;
|
||||
const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.3 * Math.sin(7 * t + Math.random()));
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
const x = r * Math.cos(a);
|
||||
const z = r * Math.sin(a);
|
||||
arr[i * 3] = x;
|
||||
arr[i * 3 + 1] = y;
|
||||
arr[i * 3 + 2] = z;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function computeStarTargets(count) {
|
||||
const arr = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
const rBase = 0.75;
|
||||
const r = rBase + 0.25 * Math.cos(5 * a) + (Math.random() - 0.5) * 0.12;
|
||||
const x = r * Math.cos(a);
|
||||
const z = r * Math.sin(a);
|
||||
const y = treeHeight + 0.9 + (Math.random() - 0.5) * 0.35;
|
||||
arr[i * 3] = x;
|
||||
arr[i * 3 + 1] = y;
|
||||
arr[i * 3 + 2] = z;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function computeAccentsTargets(count) {
|
||||
const arr = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const y = Math.random() * treeHeight;
|
||||
const t = y / treeHeight;
|
||||
const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.35 * Math.sin(6 * t + Math.random()));
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
const x = r * Math.cos(a);
|
||||
const z = r * Math.sin(a);
|
||||
arr[i * 3] = x;
|
||||
arr[i * 3 + 1] = y;
|
||||
arr[i * 3 + 2] = z;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
function ornamentColors(index) {
|
||||
const palette = [
|
||||
new THREE.Color(0xff4d6d), new THREE.Color(0xffb703), new THREE.Color(0x32c3ff),
|
||||
new THREE.Color(0x8ce99a), new THREE.Color(0xb197fc), new THREE.Color(0xffaad4)
|
||||
];
|
||||
return palette[index % palette.length];
|
||||
}
|
||||
|
||||
|
||||
function initSet(count, size, colorFn, computeTargets) {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
const pos = new Float32Array(count * 3);
|
||||
const col = new Float32Array(count * 3);
|
||||
const vel = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ix = i * 3;
|
||||
pos[ix] = (Math.random() - 0.5) * 0.4;
|
||||
pos[ix + 1] = (Math.random() - 0.5) * 0.4;
|
||||
pos[ix + 2] = (Math.random() - 0.5) * 0.4;
|
||||
let vx = (Math.random() - 0.5);
|
||||
let vy = (Math.random() - 0.5) * 0.6;
|
||||
let vz = (Math.random() - 0.5);
|
||||
const s = 0.012 + Math.random() * 0.018;
|
||||
vel[ix] = vx * s;
|
||||
vel[ix + 1] = vy * s;
|
||||
vel[ix + 2] = vz * s;
|
||||
const c = colorFn(i);
|
||||
col[ix] = c.r;
|
||||
col[ix + 1] = c.g;
|
||||
col[ix + 2] = c.b;
|
||||
}
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||
geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
|
||||
const mat = new THREE.PointsMaterial({ size, sizeAttenuation: true, transparent: true, opacity: 0.95, vertexColors: true, blending: THREE.AdditiveBlending, depthWrite: false });
|
||||
const points = new THREE.Points(geo, mat);
|
||||
const targets = computeTargets(count);
|
||||
group.add(points);
|
||||
return { geo, pos, col, vel, mat, points, targets };
|
||||
}
|
||||
|
||||
function initThree() {
|
||||
scene = new THREE.Scene();
|
||||
scene.fog = new THREE.FogExp2(0x06101a, 0.035);
|
||||
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(0, 6.5, 20);
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
containerEl.value.appendChild(renderer.domElement);
|
||||
|
||||
group = new THREE.Group();
|
||||
scene.add(group);
|
||||
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 0.35);
|
||||
scene.add(ambient);
|
||||
const point = new THREE.PointLight(0xfff3e0, 3.2, 100);
|
||||
point.position.set(4, treeHeight + 2, 6);
|
||||
scene.add(point);
|
||||
({ geo: geoLeaves, pos: posLeaves, col: colLeaves, vel: velLeaves, mat: matLeaves, points: pointsLeaves, targets: tgtLeaves } = initSet(
|
||||
particleCountLeaves,
|
||||
0.12,
|
||||
() => {
|
||||
const g = 0.35 + Math.random() * 0.65;
|
||||
const r = 0.2 + 0.8 * Math.random();
|
||||
const green = 0.5 + 0.5 * Math.random();
|
||||
return new THREE.Color(r * g, green * g, 0.2 * g);
|
||||
},
|
||||
computeLeafTargets
|
||||
));
|
||||
|
||||
({ geo: geoOrnaments, pos: posOrnaments, col: colOrnaments, vel: velOrnaments, mat: matOrnaments, points: pointsOrnaments, targets: tgtOrnaments } = initSet(
|
||||
particleCountOrnaments,
|
||||
0.14,
|
||||
(i) => ornamentColors(i),
|
||||
computeOrnamentTargets
|
||||
));
|
||||
|
||||
({ geo: geoTrunk, pos: posTrunk, col: colTrunk, vel: velTrunk, mat: matTrunk, points: pointsTrunk, targets: tgtTrunk } = initSet(
|
||||
particleCountTrunk,
|
||||
0.12,
|
||||
() => new THREE.Color(0.36, 0.24, 0.18),
|
||||
computeTrunkTargets
|
||||
));
|
||||
({ geo: geoStar, pos: posStar, col: colStar, vel: velStar, mat: matStar, points: pointsStar, targets: tgtStar } = initSet(
|
||||
particleCountStar,
|
||||
0.12,
|
||||
() => new THREE.Color(1.0, 0.83, 0.3),
|
||||
computeStarTargets
|
||||
));
|
||||
({ geo: geoAccents, pos: posAccents, col: colAccents, vel: velAccents, mat: matAccents, points: pointsAccents, targets: tgtAccents } = initSet(
|
||||
particleCountAccents,
|
||||
0.22,
|
||||
(i) => ornamentColors(i + 3),
|
||||
computeAccentsTargets
|
||||
));
|
||||
|
||||
const snowTex = createSnowflakeTexture();
|
||||
const glowTex = createSnowGlowTexture();
|
||||
snowGroup = new THREE.Group();
|
||||
snowGroup.renderOrder = 0;
|
||||
scene.add(snowGroup);
|
||||
snowSprites = new Array(snowCount);
|
||||
const snowHalos = new Array(snowCount);
|
||||
snowVel = new Float32Array(snowCount * 2);
|
||||
snowRot = new Float32Array(snowCount);
|
||||
snowScale = new Float32Array(snowCount);
|
||||
snowOpacityPhase = new Float32Array(snowCount);
|
||||
for (let i = 0; i < snowCount; i++) {
|
||||
const mat = new THREE.SpriteMaterial({ map: snowTex, color: 0xffffff, transparent: true, opacity: 1.0, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
|
||||
const spr = new THREE.Sprite(mat);
|
||||
const s = (1.6 + Math.random() * 1.6) * (2/3);
|
||||
spr.scale.set(s, s, 1);
|
||||
const x = (Math.random() - 0.5) * 120;
|
||||
const y = 20 + Math.random() * 40;
|
||||
const z = -22 - Math.random() * 12;
|
||||
spr.position.set(x, y, z);
|
||||
snowScale[i] = s;
|
||||
snowVel[i * 2] = (Math.random() - 0.5) * 0.006;
|
||||
snowVel[i * 2 + 1] = -0.008 - Math.random() * 0.01;
|
||||
snowRot[i] = (-0.004 + Math.random() * 0.008);
|
||||
snowOpacityPhase[i] = Math.random() * Math.PI * 2;
|
||||
snowSprites[i] = spr;
|
||||
snowGroup.add(spr);
|
||||
const haloMat = new THREE.SpriteMaterial({ map: glowTex, color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
|
||||
const halo = new THREE.Sprite(haloMat);
|
||||
halo.scale.set(s * 2.6, s * 2.6, 1);
|
||||
halo.position.copy(spr.position);
|
||||
snowHalos[i] = halo;
|
||||
snowGroup.add(halo);
|
||||
}
|
||||
pointsLeaves.renderOrder = 1;
|
||||
pointsOrnaments.renderOrder = 1;
|
||||
pointsTrunk.renderOrder = 1;
|
||||
pointsStar.renderOrder = 1;
|
||||
pointsAccents.renderOrder = 1;
|
||||
fitCamera();
|
||||
startTime = performance.now();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!running) return;
|
||||
const now = performance.now();
|
||||
const elapsed = now - startTime;
|
||||
function updateSet(geo, posArr, velArr, startMorphArr, targetsArr, count) {
|
||||
const attr = geo.getAttribute('position');
|
||||
const arr = attr.array;
|
||||
if (elapsed < disperseDuration) {
|
||||
const dt = 16;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ix = i * 3;
|
||||
arr[ix] += velArr[ix] * dt;
|
||||
arr[ix + 1] += velArr[ix + 1] * dt;
|
||||
arr[ix + 2] += velArr[ix + 2] * dt;
|
||||
}
|
||||
} else {
|
||||
if (!startMorphArr) {
|
||||
startMorphArr = Float32Array.from(arr);
|
||||
if (geo === geoLeaves) startMorphLeaves = startMorphArr;
|
||||
if (geo === geoOrnaments) startMorphOrnaments = startMorphArr;
|
||||
if (geo === geoTrunk) startMorphTrunk = startMorphArr;
|
||||
if (geo === geoStar) startMorphStar = startMorphArr;
|
||||
if (geo === geoAccents) startMorphAccents = startMorphArr;
|
||||
}
|
||||
const t = Math.min(1, (elapsed - disperseDuration) / morphDuration);
|
||||
const e = easeInOutCubic(t);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ix = i * 3;
|
||||
arr[ix] = startMorphArr[ix] + (targetsArr[ix] - startMorphArr[ix]) * e;
|
||||
arr[ix + 1] = startMorphArr[ix + 1] + (targetsArr[ix + 1] - startMorphArr[ix + 1]) * e;
|
||||
arr[ix + 2] = startMorphArr[ix + 2] + (targetsArr[ix + 2] - startMorphArr[ix + 2]) * e;
|
||||
}
|
||||
}
|
||||
attr.needsUpdate = true;
|
||||
}
|
||||
|
||||
const dt = 16;
|
||||
for (let i = 0; i < snowCount; i++) {
|
||||
const spr = snowSprites[i];
|
||||
spr.position.x += snowVel[i * 2] * dt;
|
||||
spr.position.y += snowVel[i * 2 + 1] * dt;
|
||||
spr.material.rotation += snowRot[i];
|
||||
const f = Math.min(1.0, 0.9 + 0.45 * Math.abs(Math.sin(now * 0.0015 + snowOpacityPhase[i])));
|
||||
spr.material.opacity = 0.67 * f;
|
||||
const halo = snowGroup.children[(i * 2) + 1];
|
||||
if (halo) {
|
||||
halo.position.copy(spr.position);
|
||||
halo.material.opacity = Math.min(1.0, 0.47 * f);
|
||||
}
|
||||
if (spr.position.y < -30) {
|
||||
spr.position.set((Math.random() - 0.5) * 120, 20 + Math.random() * 40, -22 - Math.random() * 12);
|
||||
spr.material.rotation = Math.random() * Math.PI * 2;
|
||||
if (halo) {
|
||||
halo.position.copy(spr.position);
|
||||
halo.material.rotation = spr.material.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSet(geoLeaves, posLeaves, velLeaves, startMorphLeaves, tgtLeaves, particleCountLeaves);
|
||||
updateSet(geoOrnaments, posOrnaments, velOrnaments, startMorphOrnaments, tgtOrnaments, particleCountOrnaments);
|
||||
updateSet(geoTrunk, posTrunk, velTrunk, startMorphTrunk, tgtTrunk, particleCountTrunk);
|
||||
updateSet(geoStar, posStar, velStar, startMorphStar, tgtStar, particleCountStar);
|
||||
updateSet(geoAccents, posAccents, velAccents, startMorphAccents, tgtAccents, particleCountAccents);
|
||||
group.rotation.y += 0.0032;
|
||||
renderer.render(scene, camera);
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
fitCamera();
|
||||
}
|
||||
|
||||
function fitCamera() {
|
||||
const aspect = camera.aspect;
|
||||
const vfov = THREE.MathUtils.degToRad(camera.fov);
|
||||
const halfH = (topY - bottomY) * 0.5 * 1.06;
|
||||
const radNeeded = (baseRadius + 1.6) * 1.06;
|
||||
const zH = halfH / Math.tan(vfov / 2);
|
||||
const zW = radNeeded / (Math.tan(vfov / 2) * aspect);
|
||||
const z = Math.max(zH, zW);
|
||||
camera.position.z = Math.max(z, 20);
|
||||
const centerY = (topY + bottomY) * 0.5;
|
||||
camera.position.y = centerY;
|
||||
camera.lookAt(0, centerY, 0);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const el = document.createElement('div');
|
||||
el.id = 'canvas-container';
|
||||
document.getElementById('app').appendChild(el);
|
||||
containerEl.value = el;
|
||||
initThree();
|
||||
setInterval(spawnBarrage, 1200);
|
||||
document.addEventListener('fullscreenchange', () => { isFs.value = !!document.fullscreenElement; });
|
||||
});
|
||||
|
||||
return () => h('div', { style: { width: '100%', height: '100%' } }, [
|
||||
h('div', { class: 'barrage' }, barrageItems.value.map(item => h('div', {
|
||||
key: item.id,
|
||||
class: 'barrage-item',
|
||||
style: {
|
||||
top: item.top,
|
||||
color: item.color,
|
||||
animationDuration: item.duration,
|
||||
}
|
||||
}, item.text))),
|
||||
h('button', { class: 'fullscreen-btn', onClick: toggleFs }, isFs.value ? '退出全屏' : '全屏'),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
createApp(App).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
39
InfoGenie-frontend/public/toolbox/网页小玩具/网页挂钟/index.html
Normal file
39
InfoGenie-frontend/public/toolbox/网页小玩具/网页挂钟/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
#clock {
|
||||
font-size: 48px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="clock">00:00:00</div>
|
||||
<script>
|
||||
function updateTime() {
|
||||
const clock = document.getElementById('clock');
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
clock.textContent = `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
setInterval(updateTime, 1000);
|
||||
updateTime();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
177
InfoGenie-frontend/public/toolbox/网页小玩具/随机Emoji表情/index.html
Normal file
177
InfoGenie-frontend/public/toolbox/网页小玩具/随机Emoji表情/index.html
Normal file
@@ -0,0 +1,177 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>随机Emoji表情</title>
|
||||
<meta name="description" content="清新风格随机Emoji表情包展示,支持复制与刷新,移动端与电脑端自适应。" />
|
||||
<style>
|
||||
:root {
|
||||
--bg-start: #eaf9e9;
|
||||
--bg-end: #f4ffe5;
|
||||
--panel-bg: rgba(255,255,255,0.60);
|
||||
--panel-bd: rgba(255,255,255,0.85);
|
||||
--accent: #79c86b;
|
||||
--text: #253525;
|
||||
}
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
|
||||
background-attachment: fixed;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
/* 隐藏 Webkit 浏览器的滚动条 */
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
html {
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
html::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.wrap {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.panel {
|
||||
width: min(92vw, 1100px);
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-bd);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.08);
|
||||
backdrop-filter: saturate(1.1) blur(2px);
|
||||
padding: 1rem 1rem 1.25rem;
|
||||
}
|
||||
.header { display:flex; justify-content:space-between; align-items:baseline; gap:.75rem; margin-bottom:.75rem; }
|
||||
.title { font-weight:600; font-size: clamp(1.1rem, 2.6vw, 1.6rem); letter-spacing:.3px; }
|
||||
.hint { font-size: clamp(.85rem, 2vw, 1rem); color:#4a6f4a; opacity:.9; }
|
||||
|
||||
table { width: 100%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; overflow: hidden; border-radius: 12px; background: rgba(255,255,255,0.38); }
|
||||
tbody { width: 100%; }
|
||||
td {
|
||||
border: 1px solid rgba(255,255,255,0.75);
|
||||
text-align: center; vertical-align: middle; user-select: none; cursor: pointer;
|
||||
background: rgba(255,255,255,0.50);
|
||||
transition: transform 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
||||
font-size: var(--cell-font);
|
||||
height: var(--cell-size);
|
||||
line-height: 1;
|
||||
}
|
||||
td:hover { background: rgba(255,255,255,0.80); transform: scale(1.06); box-shadow: inset 0 0 0 2px rgba(121,200,107,0.18); }
|
||||
|
||||
.controls { display:flex; justify-content:center; align-items:center; gap:.8rem; margin-top:1rem; }
|
||||
.refresh { border:none; padding:.6rem 1.15rem; border-radius:999px; font-weight:600; letter-spacing:.3px; color:#1f351f; background: linear-gradient(180deg, #c9f6c9, #c9f2a8); box-shadow: 0 6px 14px rgba(121,200,107,0.28), inset 0 1px 0 rgba(255,255,255,0.85); transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; }
|
||||
.refresh:hover { transform: translateY(-1px); filter: saturate(1.06); }
|
||||
.refresh:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(121,200,107,0.28); }
|
||||
|
||||
.toast {
|
||||
position: fixed; left:50%; bottom: 24px; transform: translateX(-50%) translateY(80px);
|
||||
background: rgba(255,255,255,0.95); color:#1f351f; border:1px solid rgba(255,255,255,0.95);
|
||||
border-radius: 10px; box-shadow: 0 8px 18px rgba(0,0,0,0.12);
|
||||
padding: .5rem .85rem; font-weight:600; opacity:0; pointer-events:none; transition: opacity 160ms ease, transform 160ms ease;
|
||||
}
|
||||
.toast.show { opacity:1; transform: translateX(-50%) translateY(0); }
|
||||
|
||||
/* 基础尺寸,JS 会根据 10×10 或 20×20 自动适配 */
|
||||
:root { --cell-size: 48px; --cell-font: 28px; }
|
||||
@media (max-width: 768px) and (orientation: portrait) {
|
||||
:root { --cell-size: clamp(40px, 7.2vw, 58px); --cell-font: clamp(22px, 5.8vw, 32px); }
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
:root { --cell-size: clamp(44px, 3.6vw, 58px); --cell-font: clamp(22px, 2.2vw, 32px); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="panel">
|
||||
<div class="header">
|
||||
<div class="title">随机 Emoji 表情</div>
|
||||
<div class="hint">点击任意 Emoji 复制</div>
|
||||
</div>
|
||||
<table id="emoji-table" aria-label="随机 Emoji 表格"><tbody id="emoji-body"></tbody></table>
|
||||
<div class="controls"><button class="refresh" id="refresh-btn">刷新</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast">已复制!</div>
|
||||
|
||||
<script>
|
||||
// 随机生成 Emoji(从常用的 Unicode 区间中抽取)
|
||||
function randomEmoji() {
|
||||
const ranges = [
|
||||
[0x1F600, 0x1F64F], // Emoticons
|
||||
[0x1F300, 0x1F5FF], // Misc Symbols and Pictographs
|
||||
[0x1F680, 0x1F6FF], // Transport & Map
|
||||
[0x2600, 0x26FF], // Misc symbols
|
||||
[0x2700, 0x27BF], // Dingbats
|
||||
[0x1F900, 0x1F9FF], // Supplemental Symbols and Pictographs
|
||||
[0x1FA70, 0x1FAFF] // Symbols & Pictographs Extended-A
|
||||
];
|
||||
const [start, end] = ranges[Math.floor(Math.random() * ranges.length)];
|
||||
const code = start + Math.floor(Math.random() * (end - start + 1));
|
||||
return String.fromCodePoint(code);
|
||||
}
|
||||
|
||||
const tableBody = document.getElementById('emoji-body');
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
const toastEl = document.getElementById('toast');
|
||||
|
||||
function isPortraitMobile() {
|
||||
return window.matchMedia('(max-width: 768px) and (orientation: portrait)').matches;
|
||||
}
|
||||
function getGridSize() { return isPortraitMobile() ? 10 : 20; }
|
||||
|
||||
function showToast(text) {
|
||||
toastEl.textContent = text;
|
||||
toastEl.classList.add('show');
|
||||
clearTimeout(showToast._timer);
|
||||
showToast._timer = setTimeout(() => toastEl.classList.remove('show'), 1200);
|
||||
}
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
try { await navigator.clipboard.writeText(text); }
|
||||
catch (e) {
|
||||
const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove();
|
||||
}
|
||||
showToast(`已复制: ${text}`);
|
||||
}
|
||||
|
||||
function generateTable() {
|
||||
const size = getGridSize();
|
||||
tableBody.innerHTML = '';
|
||||
for (let r = 0; r < size; r++) {
|
||||
const tr = document.createElement('tr');
|
||||
for (let c = 0; c < size; c++) {
|
||||
const td = document.createElement('td');
|
||||
const e = randomEmoji();
|
||||
td.textContent = e; td.title = '点击复制';
|
||||
td.addEventListener('click', () => copyToClipboard(e));
|
||||
tr.appendChild(td);
|
||||
}
|
||||
tableBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener('click', generateTable);
|
||||
window.addEventListener('resize', () => {
|
||||
const newSize = getGridSize();
|
||||
if (tableBody.children.length !== newSize) generateTable();
|
||||
});
|
||||
|
||||
generateTable();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user