style: 添加预设样式
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
<style>
|
||||
#matrix-rain-container {
|
||||
width: 100%; height: 100%; background: #000;
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
#matrix-canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
<div id="matrix-rain-container">
|
||||
<canvas id="matrix-canvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('matrix-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer'); // Or document.body if not in that specific env
|
||||
if (!canvas || !layerElement) {
|
||||
console.error("Canvas or layerElement not found.");
|
||||
return;
|
||||
}
|
||||
const ctx = canvas.getContext('2d');
|
||||
let animationFrameId;
|
||||
const characters = 'アァカサタナハマヤャラワABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const fontSize = 14;
|
||||
let columns, drops = [];
|
||||
|
||||
// --- Slowdown Configuration ---
|
||||
const slowdownFactor = 10; // 数字越大,动画越慢 (例如 2 = 半速, 3 = 1/3速度)
|
||||
const originalAlpha = 0.1; // 原始的背景淡出alpha值
|
||||
|
||||
// 计算调整后的alpha值,以保持拖尾视觉效果
|
||||
// 如果 slowdownFactor 为 1, adjustedAlpha 等于 originalAlpha
|
||||
// (1 - newAlpha)^slowdownFactor = (1 - originalAlpha)^1
|
||||
// 1 - newAlpha = (1 - originalAlpha)^(1/slowdownFactor)
|
||||
// newAlpha = 1 - (1 - originalAlpha)^(1/slowdownFactor)
|
||||
const adjustedAlpha = slowdownFactor <= 1 ? originalAlpha : 1 - Math.pow(1 - originalAlpha, 1 / slowdownFactor);
|
||||
|
||||
let frameCounter = 0; // 用于控制逻辑更新频率的计数器
|
||||
// --- End Slowdown Configuration ---
|
||||
|
||||
|
||||
function setupDimensionsAndColumns() {
|
||||
canvas.width = layerElement.offsetWidth;
|
||||
canvas.height = layerElement.offsetHeight;
|
||||
columns = Math.floor(canvas.width / fontSize);
|
||||
drops = [];
|
||||
for (let x = 0; x < columns; x++) {
|
||||
drops[x] = 1 + Math.floor(Math.random() * (canvas.height / fontSize));
|
||||
}
|
||||
frameCounter = 0; // 重置帧计数器
|
||||
}
|
||||
|
||||
function drawMatrix() {
|
||||
if (!ctx || canvas.width === 0 || canvas.height === 0) {
|
||||
animationFrameId = requestAnimationFrame(drawMatrix);
|
||||
return;
|
||||
}
|
||||
|
||||
// 背景淡出效果:每一帧都执行,使用调整后的alpha值
|
||||
ctx.fillStyle = `rgba(0, 0, 0, ${adjustedAlpha})`;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
frameCounter++;
|
||||
|
||||
// 只有当帧计数器达到 slowdownFactor 时,才更新雨滴逻辑和绘制字符
|
||||
if (frameCounter >= slowdownFactor) {
|
||||
frameCounter = 0; // 重置计数器
|
||||
|
||||
ctx.fillStyle = '#0F0'; // 雨滴颜色
|
||||
ctx.font = fontSize + 'px monospace';
|
||||
|
||||
for (let i = 0; i < drops.length; i++) {
|
||||
const text = characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
||||
|
||||
// 如果雨滴超出画布底部,并且随机条件满足,则重置雨滴到顶部
|
||||
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
|
||||
drops[i] = 0;
|
||||
}
|
||||
drops[i]++; // 雨滴下落
|
||||
}
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(drawMatrix);
|
||||
}
|
||||
|
||||
const debounce = (func, delay) => {
|
||||
let t;
|
||||
return (...a) => {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => func.apply(this, a), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const debouncedReinitialize = debounce(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
setupDimensionsAndColumns();
|
||||
if (columns > 0) {
|
||||
drawMatrix();
|
||||
}
|
||||
}, 250);
|
||||
|
||||
// 确保 layerElement 存在才进行监听
|
||||
if (layerElement) {
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target === layerElement) {
|
||||
debouncedReinitialize();
|
||||
}
|
||||
});
|
||||
});
|
||||
resizeObserver.observe(layerElement);
|
||||
} else {
|
||||
// Fallback if layerElement is not specific, e.g. observe window or body
|
||||
// window.addEventListener('resize', debouncedReinitialize); // Example fallback
|
||||
console.warn("layerElement not found for ResizeObserver, resize handling might be limited.");
|
||||
}
|
||||
|
||||
setupDimensionsAndColumns();
|
||||
if (columns > 0) {
|
||||
drawMatrix();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,164 @@
|
||||
<style>
|
||||
#color-ribbons-container {
|
||||
width: 100%; height: 100%;
|
||||
background-color: #222733; /* 柔和的深蓝灰色 */
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
#ribbons-canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
<div id="color-ribbons-container">
|
||||
<canvas id="ribbons-canvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('ribbons-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer') || canvas.parentElement;
|
||||
if (!canvas || !layerElement) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width, height, animationFrameId;
|
||||
const ribbons = [];
|
||||
const numRibbons = 5; // 彩带数量
|
||||
let time = 0;
|
||||
|
||||
// HSL颜色辅助函数
|
||||
function hslToRgb(h, s, l) {
|
||||
let r, g, b;
|
||||
if (s == 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
class Ribbon {
|
||||
constructor() {
|
||||
this.numPoints = 50 + Math.floor(Math.random() * 30); // 组成彩带路径的点数
|
||||
this.points = [];
|
||||
this.baseY = height * (0.2 + Math.random() * 0.6);
|
||||
this.amplitude = height * (0.05 + Math.random() * 0.15);
|
||||
this.frequency = 0.002 + Math.random() * 0.003;
|
||||
this.phase = Math.random() * Math.PI * 2;
|
||||
this.thickness = 20 + Math.random() * 30; // 彩带厚度
|
||||
this.speed = 0.5 + Math.random() * 0.5; // 彩带横向移动速度
|
||||
this.hueStart = Math.random(); // 0-1
|
||||
this.hueChangeSpeed = 0.0005 + Math.random() * 0.001;
|
||||
this.opacity = 0.1 + Math.random() * 0.2;
|
||||
this.xOffset = -width * Math.random(); // 初始横向偏移,让彩带从不同位置进入
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.xOffset += this.speed * (deltaTime / (1000/60)); // Normalize speed to 60fps
|
||||
if (this.xOffset > width + this.thickness * 5) { // 彩带完全移出右侧
|
||||
this.xOffset = -this.thickness * 5 - Math.random() * width * 0.5; // 从左侧重新进入
|
||||
this.baseY = height * (0.2 + Math.random() * 0.6); // 改变Y基线
|
||||
this.hueStart = Math.random(); // 改变颜色
|
||||
}
|
||||
this.hueStart = (this.hueStart + this.hueChangeSpeed) % 1;
|
||||
|
||||
this.points = [];
|
||||
for (let i = 0; i < this.numPoints; i++) {
|
||||
const progress = i / (this.numPoints - 1); // 0 to 1
|
||||
const x = this.xOffset + progress * (width + this.thickness * 10); // 彩带绘制范围比屏幕宽
|
||||
const y = this.baseY + Math.sin(x * this.frequency + this.phase + time * 0.01) * this.amplitude +
|
||||
Math.cos(x * this.frequency * 0.5 + this.phase * 0.7 + time * 0.005) * this.amplitude * 0.5;
|
||||
this.points.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.points.length < 2) return;
|
||||
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < this.points.length -1; i++) {
|
||||
const p1 = this.points[i];
|
||||
const p2 = this.points[i+1];
|
||||
|
||||
// 计算垂直于路径的偏移点,形成彩带的上下边缘
|
||||
const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
|
||||
const dxTop = Math.sin(angle) * this.thickness / 2;
|
||||
const dyTop = -Math.cos(angle) * this.thickness / 2;
|
||||
const dxBottom = -Math.sin(angle) * this.thickness / 2;
|
||||
const dyBottom = Math.cos(angle) * this.thickness / 2;
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(p1.x + dxTop, p1.y + dyTop);
|
||||
} else {
|
||||
ctx.lineTo(p1.x + dxTop, p1.y + dyTop);
|
||||
}
|
||||
}
|
||||
// 绘制下边缘 (反向)
|
||||
for (let i = this.points.length - 1; i > 0; i--) {
|
||||
const p1 = this.points[i];
|
||||
// p2 is points[i-1] but we use p1's angle approx or recalculate
|
||||
const p_prev = this.points[i-1];
|
||||
const angle = Math.atan2(p1.y - p_prev.y, p1.x - p_prev.x); // Angle from prev to current
|
||||
const dxBottom = -Math.sin(angle) * this.thickness / 2;
|
||||
const dyBottom = Math.cos(angle) * this.thickness / 2;
|
||||
ctx.lineTo(p1.x + dxBottom, p1.y + dyBottom);
|
||||
}
|
||||
ctx.closePath();
|
||||
|
||||
const [r,g,b] = hslToRgb(this.hueStart, 0.7, 0.6); // (饱和度0.7, 亮度0.6)
|
||||
ctx.fillStyle = `rgba(${r},${g},${b}, ${this.opacity})`;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
width = layerElement.offsetWidth;
|
||||
height = layerElement.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ribbons.length = 0;
|
||||
for (let i = 0; i < numRibbons; i++) {
|
||||
ribbons.push(new Ribbon());
|
||||
}
|
||||
// 使用 'lighter' 或 'screen' 混合模式让彩带叠加时颜色混合更好
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
}
|
||||
|
||||
let lastTime = 0;
|
||||
function animate(currentTime) {
|
||||
const deltaTime = currentTime - lastTime;
|
||||
lastTime = currentTime;
|
||||
time++;
|
||||
|
||||
ctx.clearRect(0, 0, width, height); // 清晰的背景,或者用低alpha填充制造拖尾
|
||||
|
||||
ribbons.forEach(ribbon => {
|
||||
ribbon.update(deltaTime || (1000/60)); // Provide a fallback for deltaTime on first frame
|
||||
ribbon.draw();
|
||||
});
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
init();
|
||||
// animate(0); // Restart animation loop
|
||||
}, 250);
|
||||
}
|
||||
|
||||
init();
|
||||
animate(0); // Start animation loop
|
||||
window.addEventListener('resize', onResize);
|
||||
canvas.cleanup = function() { if (animationFrameId) cancelAnimationFrame(animationFrameId); };
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<style>
|
||||
#svg-lava-lamp-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #111827; /* 深灰蓝 */
|
||||
overflow: hidden;
|
||||
}
|
||||
#svg-lava-lamp-background svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.lava-g {
|
||||
filter: url(#gooeyFilter); /* 应用"粘稠"滤镜 */
|
||||
}
|
||||
|
||||
.lava-blob {
|
||||
fill: #ffcc00; /* 岩浆颜色 */
|
||||
/* 动画由JS控制更灵活,但这里用CSS尝试 */
|
||||
animation: moveBlob 15s infinite ease-in-out alternate;
|
||||
}
|
||||
.lava-blob:nth-child(1) { animation-duration: 18s; animation-delay: -2s; fill: #ff9900;}
|
||||
.lava-blob:nth-child(2) { animation-duration: 12s; animation-delay: -5s; fill: #ff6600;}
|
||||
.lava-blob:nth-child(3) { animation-duration: 20s; animation-delay: 0s; fill: #ff3300;}
|
||||
.lava-blob:nth-child(4) { animation-duration: 16s; animation-delay: -8s; fill: #cc3300;}
|
||||
.lava-blob:nth-child(5) { animation-duration: 14s; animation-delay: -12s;fill: #ffcc00;}
|
||||
|
||||
|
||||
@keyframes moveBlob {
|
||||
0% { transform: translate(0px, 0px) scale(1); }
|
||||
25% { transform: translate(30px, -50px) scale(1.2); }
|
||||
50% { transform: translate(-20px, 40px) scale(0.8); }
|
||||
75% { transform: translate(10px, -30px) scale(1.1); }
|
||||
100% { transform: translate(0px, 0px) scale(1); }
|
||||
}
|
||||
</style>
|
||||
<div id="svg-lava-lamp-background">
|
||||
<svg viewBox="0 0 300 400" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="gooeyFilter">
|
||||
<!-- 高斯模糊使边缘模糊 -->
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blur" />
|
||||
<!-- 颜色矩阵增加alpha对比度,使模糊的边缘更"粘稠"地融合 -->
|
||||
<feColorMatrix in="blur" mode="matrix"
|
||||
values="1 0 0 0 0
|
||||
0 1 0 0 0
|
||||
0 0 1 0 0
|
||||
0 0 0 25 -10"
|
||||
result="gooey" />
|
||||
<!-- 可选:将原始图形叠在上面,如果想要更清晰的中心 -->
|
||||
<!-- <feComposite in="SourceGraphic" in2="gooey" operator="atop"/> -->
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 将所有气泡放在一个应用了滤镜的组中 -->
|
||||
<g class="lava-g">
|
||||
<!-- 定义一些圆形作为气泡 -->
|
||||
<circle class="lava-blob" cx="100" cy="300" r="40" />
|
||||
<circle class="lava-blob" cx="200" cy="350" r="55" />
|
||||
<circle class="lava-blob" cx="150" cy="100" r="30" />
|
||||
<circle class="lava-blob" cx="50" cy="150" r="45" />
|
||||
<circle class="lava-blob" cx="250" cy="200" r="60" />
|
||||
<circle class="lava-blob" cx="120" cy="220" r="35" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
// 用JS可以更精细地控制每个blob的动画,例如使用随机目标点
|
||||
const blobs = document.querySelectorAll('#svg-lava-lamp-background .lava-blob');
|
||||
const svg = document.querySelector('#svg-lava-lamp-background svg');
|
||||
if (!blobs.length || !svg) return;
|
||||
|
||||
const viewBoxWidth = svg.viewBox.baseVal.width;
|
||||
const viewBoxHeight = svg.viewBox.baseVal.height;
|
||||
|
||||
blobs.forEach((blob, index) => {
|
||||
// 可以用JS设置更复杂的动画,这里CSS动画已经够用
|
||||
// 为了让CSS动画不完全同步,已经用了nth-child和animation-delay
|
||||
// 如果想完全随机化CSS动画的参数,可以用JS来动态设置style属性
|
||||
// 例如: blob.style.animationDuration = (10 + Math.random() * 10) + 's';
|
||||
// blob.style.animationDelay = (-Math.random() * 5) + 's';
|
||||
|
||||
// 动态改变transform-origin让缩放更有趣
|
||||
blob.style.transformOrigin = `${Math.random()*50+25}% ${Math.random()*50+25}%`;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<style>
|
||||
#css-3d-cubes-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #080010; /* 深紫色背景 */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
perspective: 1000px; /* 为3D效果设置透视 */
|
||||
}
|
||||
|
||||
.scene {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.cube-wrapper {
|
||||
position: absolute;
|
||||
width: 100px; /* 调整立方体大小 */
|
||||
height: 100px;
|
||||
transform-style: preserve-3d;
|
||||
/* 动画由JS控制或预设不同参数 */
|
||||
}
|
||||
|
||||
.cube {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(0deg) rotateY(0deg); /* 初始旋转,会被动画覆盖 */
|
||||
animation: spin 15s infinite linear;
|
||||
}
|
||||
|
||||
.cube .face {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 1px solid rgba(0, 255, 255, 0.7); /* 青色霓虹边框 */
|
||||
background-color: rgba(0, 50, 80, 0.2); /* 半透明深蓝面 */
|
||||
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5), inset 0 0 8px rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 定位立方体的六个面 */
|
||||
.cube .front { transform: rotateY( 0deg) translateZ(50px); }
|
||||
.cube .back { transform: rotateY(180deg) translateZ(50px); }
|
||||
.cube .right { transform: rotateY( 90deg) translateZ(50px); }
|
||||
.cube .left { transform: rotateY(-90deg) translateZ(50px); }
|
||||
.cube .top { transform: rotateX( 90deg) translateZ(50px); }
|
||||
.cube .bottom { transform: rotateX(-90deg) translateZ(50px); }
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
|
||||
to { transform: rotateX(360deg) rotateY(360deg) rotateZ(360deg); }
|
||||
}
|
||||
|
||||
/* 示例立方体位置和动画差异化 */
|
||||
.cube-wrapper-1 { top: 20%; left: 30%; transform: scale(0.8) translateZ(-100px); }
|
||||
.cube-wrapper-1 .cube { animation-duration: 18s; animation-delay: -2s; }
|
||||
|
||||
.cube-wrapper-2 { top: 50%; left: 60%; transform: scale(1.2) translateZ(50px); }
|
||||
.cube-wrapper-2 .cube { animation-duration: 12s; }
|
||||
|
||||
.cube-wrapper-3 { top: 70%; left: 15%; transform: scale(0.6) translateZ(-200px); }
|
||||
.cube-wrapper-3 .cube { animation-duration: 22s; animation-delay: -5s; }
|
||||
</style>
|
||||
<div id="css-3d-cubes-background">
|
||||
<div class="scene">
|
||||
<div class="cube-wrapper cube-wrapper-1">
|
||||
<div class="cube">
|
||||
<div class="face front"></div> <div class="face back"></div>
|
||||
<div class="face right"></div> <div class="face left"></div>
|
||||
<div class="face top"></div> <div class="face bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cube-wrapper cube-wrapper-2">
|
||||
<div class="cube">
|
||||
<div class="face front"></div> <div class="face back"></div>
|
||||
<div class="face right"></div> <div class="face left"></div>
|
||||
<div class="face top"></div> <div class="face bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cube-wrapper cube-wrapper-3">
|
||||
<div class="cube">
|
||||
<div class="face front"></div> <div class="face back"></div>
|
||||
<div class="face right"></div> <div class="face left"></div>
|
||||
<div class="face top"></div> <div class="face bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可以用JS动态添加更多cube-wrapper并随机化参数 -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,31 +11,23 @@
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('matrix-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer'); // Or document.body if not in that specific env
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer');
|
||||
if (!canvas || !layerElement) {
|
||||
console.error("Canvas or layerElement not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
let animationFrameId;
|
||||
const characters = 'アァカサタナハマヤャラワABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const fontSize = 14;
|
||||
let columns, drops = [];
|
||||
|
||||
// --- Slowdown Configuration ---
|
||||
const slowdownFactor = 10; // 数字越大,动画越慢 (例如 2 = 半速, 3 = 1/3速度)
|
||||
const originalAlpha = 0.1; // 原始的背景淡出alpha值
|
||||
|
||||
// 计算调整后的alpha值,以保持拖尾视觉效果
|
||||
// 如果 slowdownFactor 为 1, adjustedAlpha 等于 originalAlpha
|
||||
// (1 - newAlpha)^slowdownFactor = (1 - originalAlpha)^1
|
||||
// 1 - newAlpha = (1 - originalAlpha)^(1/slowdownFactor)
|
||||
// newAlpha = 1 - (1 - originalAlpha)^(1/slowdownFactor)
|
||||
// 拖尾视觉效果配置
|
||||
const slowdownFactor = 10;
|
||||
const originalAlpha = 0.17;
|
||||
const adjustedAlpha = slowdownFactor <= 1 ? originalAlpha : 1 - Math.pow(1 - originalAlpha, 1 / slowdownFactor);
|
||||
|
||||
let frameCounter = 0; // 用于控制逻辑更新频率的计数器
|
||||
// --- End Slowdown Configuration ---
|
||||
|
||||
let frameCounter = 0;
|
||||
|
||||
function setupDimensionsAndColumns() {
|
||||
canvas.width = layerElement.offsetWidth;
|
||||
@@ -45,7 +37,7 @@
|
||||
for (let x = 0; x < columns; x++) {
|
||||
drops[x] = 1 + Math.floor(Math.random() * (canvas.height / fontSize));
|
||||
}
|
||||
frameCounter = 0; // 重置帧计数器
|
||||
frameCounter = 0;
|
||||
}
|
||||
|
||||
function drawMatrix() {
|
||||
@@ -54,30 +46,27 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 背景淡出效果:每一帧都执行,使用调整后的alpha值
|
||||
ctx.fillStyle = `rgba(0, 0, 0, ${adjustedAlpha})`;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
frameCounter++;
|
||||
|
||||
// 只有当帧计数器达到 slowdownFactor 时,才更新雨滴逻辑和绘制字符
|
||||
if (frameCounter >= slowdownFactor) {
|
||||
frameCounter = 0; // 重置计数器
|
||||
frameCounter = 0;
|
||||
|
||||
ctx.fillStyle = '#0F0'; // 雨滴颜色
|
||||
ctx.fillStyle = '#0F0';
|
||||
ctx.font = fontSize + 'px monospace';
|
||||
|
||||
for (let i = 0; i < drops.length; i++) {
|
||||
const text = characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
||||
|
||||
// 如果雨滴超出画布底部,并且随机条件满足,则重置雨滴到顶部
|
||||
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
|
||||
drops[i] = 0;
|
||||
}
|
||||
drops[i]++; // 雨滴下落
|
||||
drops[i]++;
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(drawMatrix);
|
||||
}
|
||||
|
||||
@@ -99,7 +88,6 @@
|
||||
}
|
||||
}, 250);
|
||||
|
||||
// 确保 layerElement 存在才进行监听
|
||||
if (layerElement) {
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
@@ -110,8 +98,6 @@
|
||||
});
|
||||
resizeObserver.observe(layerElement);
|
||||
} else {
|
||||
// Fallback if layerElement is not specific, e.g. observe window or body
|
||||
// window.addEventListener('resize', debouncedReinitialize); // Example fallback
|
||||
console.warn("layerElement not found for ResizeObserver, resize handling might be limited.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<style>
|
||||
#svg-hexgrid-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #0a0f14; /* 非常暗的冷灰色 */
|
||||
overflow: hidden;
|
||||
}
|
||||
#svg-hexgrid-background svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hexagon-bg {
|
||||
stroke: rgba(70, 100, 130, 0.25); /* 暗青蓝色背景六边形 */
|
||||
stroke-width: 1px;
|
||||
fill: rgba(30, 40, 50, 0.1);
|
||||
}
|
||||
|
||||
.trace-path {
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 600; /* 一个足够大的值,确保能覆盖路径总长 */
|
||||
stroke-dashoffset: 600; /* 初始状态,路径不可见 */
|
||||
animation: drawAndFadePath 12s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 4px currentColor) drop-shadow(0 0 8px currentColor); /* 使用当前描边色发光 */
|
||||
}
|
||||
|
||||
/* 不同路径使用不同颜色和动画延迟 */
|
||||
.path-cyan { stroke: #00e5ff; }
|
||||
.path-magenta { stroke: #ff00ff; animation-delay: -3s; }
|
||||
.path-lime { stroke: #aeff00; animation-delay: -6s; }
|
||||
.path-orange { stroke: #ffaa00; animation-delay: -9s; animation-direction: alternate; }
|
||||
|
||||
|
||||
@keyframes drawAndFadePath {
|
||||
0% { stroke-dashoffset: 600; opacity: 0; } /* 开始时完全不可见 */
|
||||
10% { opacity: 0.8; } /* 快速显现 */
|
||||
40% { stroke-dashoffset: 0; opacity: 1; } /* 路径完全绘制出来,不透明 */
|
||||
60% { stroke-dashoffset: 0; opacity: 1; } /* 保持显示 */
|
||||
90% { opacity: 0.8; } /* 开始褪色 */
|
||||
100% { stroke-dashoffset: -600; opacity: 0; } /* 路径从起点开始消失并完全透明 */
|
||||
}
|
||||
</style>
|
||||
<div id="svg-hexgrid-background">
|
||||
<svg width="100%" height="100%" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- 定义一个六边形单元,方便复用 -->
|
||||
<!-- 六边形顶点计算: r=半径, h=r*sqrt(3)/2 (内切圆半径/高度的一半) -->
|
||||
<!-- points="r,0 r/2,h -r/2,h -r,0 -r/2,-h r/2,-h" for radius r, height 2h -->
|
||||
<symbol id="hexSymbol" viewBox="-52 -45 104 90"> <!-- viewBox slightly larger than hex -->
|
||||
<polygon class="hexagon-bg" points="50,0 25,43.3 -25,43.3 -50,0 -25,-43.3 25,-43.3" />
|
||||
</symbol>
|
||||
</defs>
|
||||
|
||||
<!-- 静态六边形网格背景 -->
|
||||
<g id="staticHexGrid">
|
||||
<!-- 手动放置一些六边形作为示例,更完善的网格可以用JS生成 -->
|
||||
<use href="#hexSymbol" x="100" y="100" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="200" y="100" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="150" y="100 + 86.6*0.5" width="100" height="86.6"/> <!-- Staggered row -->
|
||||
<use href="#hexSymbol" x="250" y="100 + 86.6*0.5" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="50" y="100 + 86.6" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="300" y="100 + 86.6" width="100" height="86.6"/>
|
||||
<!-- ... 可以添加更多 for better coverage ... -->
|
||||
<use href="#hexSymbol" x="400" y="100" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="350" y="100 + 86.6*0.5" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="450" y="100 + 86.6*0.5" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="500" y="100 + 86.6" width="100" height="86.6"/>
|
||||
|
||||
<use href="#hexSymbol" x="600" y="300" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="700" y="300" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="650" y="300 + 86.6*0.5" width="100" height="86.6"/>
|
||||
<use href="#hexSymbol" x="550" y="300 + 86.6*0.5" width="100" height="86.6"/>
|
||||
</g>
|
||||
|
||||
<!-- 动态追踪线条 -->
|
||||
<g id="animatedTraces">
|
||||
<path class="trace-path path-cyan" d="M50,500 Q200,50 400,300 T750,100" />
|
||||
<path class="trace-path path-magenta" d="M750,500 Q600,50 400,300 T50,100" />
|
||||
<path class="trace-path path-lime" d="M20,150 C150,550 650,50 780,450 S 400, 200 100, 300 Z" />
|
||||
<path class="trace-path path-orange" d="M400,50 C50,250 750,350 400,550 C200,450 600,150 400,50" />
|
||||
</g>
|
||||
</svg>
|
||||
<!--
|
||||
可选JS: 动态生成六边形网格覆盖整个SVG区域
|
||||
<script>
|
||||
(function() {
|
||||
const svg = document.querySelector("#svg-hexgrid-background svg");
|
||||
const gridGroup = document.getElementById("staticHexGrid");
|
||||
if (!svg || !gridGroup) return;
|
||||
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const hexRadius = 50; // 半径
|
||||
const hexWidth = hexRadius * 2;
|
||||
const hexHeight = Math.sqrt(3) * hexRadius; // 整体高度
|
||||
const vertDist = hexHeight;
|
||||
const horizDist = hexWidth * 3/4;
|
||||
|
||||
const vbWidth = svg.viewBox.baseVal.width;
|
||||
const vbHeight = svg.viewBox.baseVal.height;
|
||||
|
||||
gridGroup.innerHTML = ''; // 清空手动放置的六边形
|
||||
|
||||
for (let y = 0, row = 0; y < vbHeight + hexHeight; y += vertDist / 2, row++) {
|
||||
for (let x = 0, col = 0; x < vbWidth + horizDist; x += horizDist, col++) {
|
||||
let currentX = x;
|
||||
if (row % 2 !== 0) { // 奇数行水平偏移
|
||||
currentX += horizDist / 2;
|
||||
}
|
||||
if (currentX > vbWidth + horizDist/2) continue; // 超出右边界
|
||||
|
||||
const use = document.createElementNS(svgNS, "use");
|
||||
use.setAttributeNS(null, "href", "#hexSymbol");
|
||||
use.setAttributeNS(null, "x", currentX);
|
||||
use.setAttributeNS(null, "y", y);
|
||||
use.setAttributeNS(null, "width", hexWidth);
|
||||
use.setAttributeNS(null, "height", hexHeight);
|
||||
gridGroup.appendChild(use);
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
-->
|
||||
</div>
|
||||
@@ -0,0 +1,203 @@
|
||||
<style>
|
||||
#elegant-network-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1d2331; /* 深灰蓝,比纯黑柔和 */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
#elegant-network-svg { width: 100%; height: 100%; position:absolute; top:0; left:0; }
|
||||
|
||||
.network-node {
|
||||
fill: rgba(150, 180, 230, 0.7); /* 柔和的节点颜色 */
|
||||
r: 2; /* 节点半径 */
|
||||
/* CSS动画驱动节点移动 */
|
||||
animation: nodeFloat 20s infinite ease-in-out alternate;
|
||||
}
|
||||
.network-node:nth-child(5n+1) { animation-duration: 22s; animation-delay: -3s; }
|
||||
.network-node:nth-child(5n+2) { animation-duration: 18s; animation-delay: -7s; fill: rgba(120,160,210,0.6); r:1.5;}
|
||||
.network-node:nth-child(5n+3) { animation-duration: 25s; animation-delay: -1s; }
|
||||
.network-node:nth-child(5n+4) { animation-duration: 19s; animation-delay: -5s; fill: rgba(170,200,240,0.8); r:2.5;}
|
||||
|
||||
|
||||
@keyframes nodeFloat { /* 定义一个通用的漂浮路径 */
|
||||
0% { transform: translate(0px, 0px); }
|
||||
25% { transform: translate(20px, -15px); }
|
||||
50% { transform: translate(-10px, 25px); }
|
||||
75% { transform: translate(15px, 5px); }
|
||||
100% { transform: translate(0px, 0px); }
|
||||
}
|
||||
|
||||
.network-line {
|
||||
stroke: rgba(100, 130, 180, 0.15); /* 非常淡的连接线 */
|
||||
stroke-width: 0.5px;
|
||||
/* 线条的显隐和动画通过JS控制 */
|
||||
}
|
||||
</style>
|
||||
<div id="elegant-network-background">
|
||||
<svg id="elegant-network-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="network-nodes-group"></g>
|
||||
<g id="network-lines-group"></g>
|
||||
</svg>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const svg = document.getElementById('elegant-network-svg');
|
||||
const nodesGroup = document.getElementById('network-nodes-group');
|
||||
const linesGroup = document.getElementById('network-lines-group');
|
||||
const layerElement = svg.closest('.terminal-custom-html-layer') || svg.parentElement;
|
||||
|
||||
if (!svg || !nodesGroup || !linesGroup || !layerElement) return;
|
||||
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
let width, height, animationFrameId;
|
||||
const nodes = [];
|
||||
const numNodes = 40; // 节点数量
|
||||
const maxDistance = 150; // 连接线最大距离
|
||||
|
||||
class Node {
|
||||
constructor(x, y, id) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.id = `node-${id}`;
|
||||
this.baseX = x; // 用于CSS动画的基准位置
|
||||
this.baseY = y;
|
||||
|
||||
this.element = document.createElementNS(svgNS, "circle");
|
||||
this.element.setAttribute("id", this.id);
|
||||
this.element.setAttribute("class", "network-node");
|
||||
// this.element.setAttribute("cx", this.x); // cx, cy 将由CSS transform 控制相对基准的偏移
|
||||
// this.element.setAttribute("cy", this.y);
|
||||
// 使用transform来应用CSS动画,而不是直接改变cx/cy
|
||||
this.element.style.transform = `translate(${this.x}px, ${this.y}px)`;
|
||||
|
||||
// 随机化CSS动画参数,让每个节点漂浮不同
|
||||
const duration = 15 + Math.random() * 15; // 15-30s
|
||||
const delay = -(Math.random() * duration);
|
||||
this.element.style.animationDuration = `${duration}s`;
|
||||
this.element.style.animationDelay = `${delay}s`;
|
||||
|
||||
nodesGroup.appendChild(this.element);
|
||||
}
|
||||
|
||||
// JS不再直接更新位置,依赖CSS动画。但需要获取当前渲染位置用于连线
|
||||
getCurrentPosition() {
|
||||
// 获取由CSS transform计算后的实际位置
|
||||
const ctm = this.element.getCTM();
|
||||
// 如果SVG嵌套复杂,可能需要更复杂的计算,或使用getBoundingClientRect
|
||||
// 对于直接在SVG根下的元素,CTM的e和f就是视口坐标系下的x和y
|
||||
// 但由于我们用translate(baseX, baseY)来设置初始位置,
|
||||
// CSS动画的transform是相对这个的。
|
||||
// 简便方法:仍然在JS中保留一个缓慢的漂移,或者接受CSS动画的位置
|
||||
// 如果依赖CSS动画,获取精确位置会复杂。
|
||||
// 为简化,这里假设CSS动画的漂移是小范围的,我们仍用js的x,y做连线,CSS只是视觉漂移
|
||||
// 或者,我们让JS也做动画,CSS只定义样式
|
||||
// 重新考虑:让JS驱动位置,CSS仅定义样式和基础动画(如闪烁)
|
||||
return { x: this.x, y: this.y }; // 简化处理
|
||||
}
|
||||
}
|
||||
|
||||
// --- 重新设计Node的动画方式:JS驱动位置,CSS负责样式 ---
|
||||
nodesGroup.innerHTML = ''; // 清空旧的
|
||||
class JsAnimatedNode {
|
||||
constructor(x, y, id) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = (Math.random() - 0.5) * 0.2; // 缓慢速度
|
||||
this.vy = (Math.random() - 0.5) * 0.2;
|
||||
this.id = `node-${id}`;
|
||||
|
||||
this.element = document.createElementNS(svgNS, "circle");
|
||||
this.element.setAttribute("id", this.id);
|
||||
this.element.classList.add("network-node"); // 应用CSS样式
|
||||
// 移除animation, 因为JS驱动位置
|
||||
this.element.style.animation = 'none';
|
||||
|
||||
// 随机化样式,比如半径和颜色 (通过覆盖CSS定义的)
|
||||
const r = 1.5 + Math.random() * 1.5; // 1.5 to 3
|
||||
this.element.setAttribute("r", r);
|
||||
if (Math.random() < 0.3) {
|
||||
this.element.style.fill = `rgba(${120 + Math.floor(Math.random()*50)}, ${150 + Math.floor(Math.random()*50)}, ${200 + Math.floor(Math.random()*50)}, ${0.5 + Math.random()*0.3})`;
|
||||
}
|
||||
|
||||
nodesGroup.appendChild(this.element);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
if (this.x < 0 || this.x > width) this.vx *= -1;
|
||||
if (this.y < 0 || this.y > height) this.vy *= -1;
|
||||
|
||||
this.element.setAttribute("cx", this.x);
|
||||
this.element.setAttribute("cy", this.y);
|
||||
}
|
||||
getCurrentPosition() { return {x: this.x, y: this.y}; }
|
||||
}
|
||||
// --- 结束重新设计 ---
|
||||
|
||||
|
||||
function connectNodes() {
|
||||
linesGroup.innerHTML = ""; // 清空旧线条
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const posA = nodes[i].getCurrentPosition();
|
||||
const posB = nodes[j].getCurrentPosition();
|
||||
const dx = posA.x - posB.x;
|
||||
const dy = posA.y - posB.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < maxDistance) {
|
||||
const line = document.createElementNS(svgNS, "line");
|
||||
line.setAttribute("x1", posA.x);
|
||||
line.setAttribute("y1", posA.y);
|
||||
line.setAttribute("x2", posB.x);
|
||||
line.setAttribute("y2", posB.y);
|
||||
line.setAttribute("class", "network-line");
|
||||
// 线条透明度随距离变化
|
||||
const opacity = Math.max(0, (1 - distance / maxDistance) * 0.3); // 0.3是基础最大透明度
|
||||
line.style.strokeOpacity = opacity.toFixed(2);
|
||||
linesGroup.appendChild(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
width = layerElement.offsetWidth;
|
||||
height = layerElement.offsetHeight;
|
||||
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
|
||||
nodes.length = 0;
|
||||
nodesGroup.innerHTML = ''; // 清空,因为JsAnimatedNode会重新添加
|
||||
linesGroup.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
// nodes.push(new Node(Math.random() * width, Math.random() * height, i)); // 旧的CSS动画Node
|
||||
nodes.push(new JsAnimatedNode(Math.random() * width, Math.random() * height, i)); // 新的JS动画Node
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
nodes.forEach(node => node.update()); // 如果是JS动画Node
|
||||
connectNodes();
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
init();
|
||||
animate();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
init();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
svg.cleanup = function() { if (animationFrameId) cancelAnimationFrame(animationFrameId); };
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,195 @@
|
||||
<style>
|
||||
#circuit-board-container {
|
||||
width: 100%; height: 100%;
|
||||
background-color: #0a192f; /* 非常深的科技蓝 */
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
#circuit-canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
<div id="circuit-board-container">
|
||||
<canvas id="circuit-canvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('circuit-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer') || canvas.parentElement;
|
||||
if (!canvas || !layerElement) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width, height, animationFrameId;
|
||||
const traces = [];
|
||||
const numTraces = 30;
|
||||
const gridSize = 40; // 网格大小,用于确定路径点
|
||||
const traceColor = "rgba(50, 180, 220, 0.15)"; // 静态轨迹颜色
|
||||
const signalColor = "rgba(100, 220, 255, 0.9)"; // 信号颜色
|
||||
const signalSpeed = 2; // 信号移动速度 (px/frame)
|
||||
|
||||
class Trace {
|
||||
constructor() {
|
||||
this.points = [];
|
||||
this.generatePath();
|
||||
this.signalPosition = 0; // 信号在路径上的当前距离
|
||||
this.signalActive = Math.random() < 0.7; // 70% 的轨迹初始有信号
|
||||
this.signalRestartDelay = 0; // 信号消失后的重启延迟
|
||||
}
|
||||
|
||||
generatePath() {
|
||||
this.points = [];
|
||||
const startX = Math.floor(Math.random() * (width / gridSize)) * gridSize;
|
||||
const startY = Math.floor(Math.random() * (height / gridSize)) * gridSize;
|
||||
this.points.push({ x: startX, y: startY });
|
||||
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
const pathLength = 5 + Math.floor(Math.random() * 10); // 路径段数
|
||||
|
||||
for (let i = 0; i < pathLength; i++) {
|
||||
const directions = []; // 0: up, 1: down, 2: left, 3: right
|
||||
if (currentY > 0) directions.push(0);
|
||||
if (currentY < height - gridSize) directions.push(1);
|
||||
if (currentX > 0) directions.push(2);
|
||||
if (currentX < width - gridSize) directions.push(3);
|
||||
|
||||
if (directions.length === 0) break; // No valid moves
|
||||
|
||||
const move = directions[Math.floor(Math.random() * directions.length)];
|
||||
let nextX = currentX, nextY = currentY;
|
||||
|
||||
switch (move) {
|
||||
case 0: nextY -= gridSize; break; // Up
|
||||
case 1: nextY += gridSize; break; // Down
|
||||
case 2: nextX -= gridSize; break; // Left
|
||||
case 3: nextX += gridSize; break; // Right
|
||||
}
|
||||
// 避免立即回头
|
||||
if (this.points.length > 1) {
|
||||
const prevPoint = this.points[this.points.length - 2];
|
||||
if (prevPoint.x === nextX && prevPoint.y === nextY) {
|
||||
i--; // Try again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.points.push({ x: nextX, y: nextY });
|
||||
currentX = nextX;
|
||||
currentY = nextY;
|
||||
}
|
||||
this.totalLength = 0;
|
||||
for(let i = 0; i < this.points.length -1; i++) {
|
||||
this.totalLength += gridSize; // All segments are gridSize long
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.signalActive) {
|
||||
this.signalPosition += signalSpeed;
|
||||
if (this.signalPosition > this.totalLength + 20) { // +20 to let it fade out
|
||||
this.signalActive = false;
|
||||
this.signalPosition = 0;
|
||||
this.signalRestartDelay = 100 + Math.random() * 300; // 100-400 frames delay
|
||||
}
|
||||
} else {
|
||||
this.signalRestartDelay--;
|
||||
if (this.signalRestartDelay <= 0 && Math.random() < 0.01) { // Low chance to reactivate
|
||||
this.signalActive = true;
|
||||
// Optional: re-generate path
|
||||
// this.generatePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.points.length < 2) return;
|
||||
|
||||
// Draw static trace
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.points[0].x, this.points[0].y);
|
||||
for (let i = 1; i < this.points.length; i++) {
|
||||
ctx.lineTo(this.points[i].x, this.points[i].y);
|
||||
}
|
||||
ctx.strokeStyle = traceColor;
|
||||
ctx.lineWidth = 1 + Math.random()*0.5;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.stroke();
|
||||
|
||||
// Draw signal
|
||||
if (this.signalActive) {
|
||||
ctx.beginPath();
|
||||
let distanceTraveled = 0;
|
||||
let signalDrawn = false;
|
||||
for (let i = 0; i < this.points.length - 1; i++) {
|
||||
const p1 = this.points[i];
|
||||
const p2 = this.points[i+1];
|
||||
const segmentLength = gridSize;
|
||||
|
||||
if (!signalDrawn && this.signalPosition >= distanceTraveled && this.signalPosition < distanceTraveled + segmentLength) {
|
||||
const progressInSegment = (this.signalPosition - distanceTraveled) / segmentLength;
|
||||
const sigX = p1.x + (p2.x - p1.x) * progressInSegment;
|
||||
const sigY = p1.y + (p2.y - p1.y) * progressInSegment;
|
||||
ctx.moveTo(sigX, sigY); // Start of signal for this frame
|
||||
signalDrawn = true; // Draw only one moveTo
|
||||
}
|
||||
if(signalDrawn && this.signalPosition + 10 > distanceTraveled) { // 10 is signal "tail" length
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
if(this.signalPosition > distanceTraveled + segmentLength + 10) {
|
||||
// break; // Signal has passed this segment
|
||||
}
|
||||
}
|
||||
if (this.signalPosition < distanceTraveled + segmentLength && signalDrawn) {
|
||||
// Draw up to the end of current segment or signal head
|
||||
const endProgress = Math.min(1, (this.signalPosition + 10 - distanceTraveled) / segmentLength); // +10 for tail
|
||||
const endX = p1.x + (p2.x - p1.x) * endProgress;
|
||||
const endY = p1.y + (p2.y - p1.y) * endProgress;
|
||||
ctx.lineTo(endX, endY);
|
||||
break; // Signal is within this segment or ends here
|
||||
}
|
||||
distanceTraveled += segmentLength;
|
||||
}
|
||||
if (signalDrawn) {
|
||||
ctx.strokeStyle = signalColor;
|
||||
ctx.lineWidth = 2 + Math.random();
|
||||
ctx.shadowColor = signalColor;
|
||||
ctx.shadowBlur = 8 + Math.random() * 5;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0; // Reset shadow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
width = layerElement.offsetWidth;
|
||||
height = layerElement.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
traces.length = 0;
|
||||
for (let i = 0; i < numTraces; i++) {
|
||||
traces.push(new Trace());
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
traces.forEach(trace => {
|
||||
trace.update();
|
||||
trace.draw();
|
||||
});
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
init();
|
||||
animate();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
init();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
canvas.cleanup = function() { if (animationFrameId) cancelAnimationFrame(animationFrameId); };
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,139 @@
|
||||
<style>
|
||||
#orbiting-rings-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #101218; /* 近黑色,略带冷调 */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
perspective: 1000px; /* 为3D旋转提供透视 */
|
||||
}
|
||||
#orbiting-rings-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
transform-style: preserve-3d; /* 允许SVG内的组进行3D变换 */
|
||||
}
|
||||
|
||||
.stardust-ring {
|
||||
transform-origin: center center; /* 确保从中心旋转 */
|
||||
/* CSS动画驱动整体旋转和倾斜 */
|
||||
animation: orbit 40s linear infinite;
|
||||
}
|
||||
.ring-particle {
|
||||
fill: rgba(180, 200, 230, 0.6); /* 淡雅的粒子颜色 */
|
||||
/* 粒子可以有自己的细微动画,如闪烁 */
|
||||
animation: particleTwinkle 5s infinite ease-in-out alternate;
|
||||
}
|
||||
|
||||
/* 不同环的参数 */
|
||||
.ring-1 { animation-duration: 50s; transform: rotateX(70deg) rotateZ(0deg) scale(1); }
|
||||
.ring-1 .ring-particle { animation-delay: -1s; }
|
||||
|
||||
.ring-2 { animation-duration: 40s; animation-direction: reverse; transform: rotateX(75deg) rotateZ(20deg) scale(0.7); }
|
||||
.ring-2 .ring-particle { fill: rgba(200, 180, 230, 0.5); animation-delay: -2.5s; }
|
||||
|
||||
.ring-3 { animation-duration: 60s; transform: rotateX(80deg) rotateZ(-15deg) scale(0.45); }
|
||||
.ring-3 .ring-particle { fill: rgba(180, 230, 200, 0.55); animation-delay: -0.5s; }
|
||||
|
||||
.ring-4 { animation-duration: 35s; animation-direction: reverse; transform: rotateX(65deg) rotateZ(30deg) scale(1.2); }
|
||||
.ring-4 .ring-particle { animation-delay: -3s; }
|
||||
|
||||
|
||||
@keyframes orbit {
|
||||
from { transform: rotateX(var(--initial-rotateX, 70deg)) rotateZ(var(--initial-rotateZ, 0deg)) rotateY(0deg) scale(var(--initial-scale,1)); }
|
||||
to { transform: rotateX(var(--initial-rotateX, 70deg)) rotateZ(var(--initial-rotateZ, 0deg)) rotateY(360deg) scale(var(--initial-scale,1)); }
|
||||
}
|
||||
/* 修正: CSS动画的transform会覆盖初始的transform。
|
||||
需要将初始的rotateX/Z/scale放入CSS变量或JS动态应用,或者在@keyframes中包含它们。
|
||||
更简单的方式是让 @keyframes orbit 只控制 rotateY,初始的X,Z,scale由.ring-N类设定。
|
||||
我们将修改.stardust-ring 的动画,使其只改变Y轴旋转。
|
||||
*/
|
||||
@keyframes orbitYOnly {
|
||||
from { transform: rotateY(0deg); } /* 这个transform会应用在.ring-N的transform之上 */
|
||||
to { transform: rotateY(360deg); }
|
||||
}
|
||||
/* 在 .stardust-ring 中使用一个包装器来应用初始变换,然后让内部元素旋转Y轴 */
|
||||
/* 或者,更简单,直接在 .ring-N 的 animation 中指定完整的 transform */
|
||||
/* 为简化,以下采用修改后的 @keyframes orbit,它会作用于已变换的元素 */
|
||||
</style>
|
||||
<div id="orbiting-rings-background">
|
||||
<svg id="orbiting-rings-svg" viewBox="-250 -250 500 500" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- JS会在这里动态生成环和粒子 -->
|
||||
</svg>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const svg = document.getElementById('orbiting-rings-svg');
|
||||
if (!svg) return;
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
|
||||
const numRings = 4;
|
||||
const particlesPerRing = 80;
|
||||
const ringBaseRadius = 80;
|
||||
|
||||
// 预定义的环参数 (可以从CSS中读取或在此处定义)
|
||||
const ringConfigs = [
|
||||
{ class: 'ring-1', initialTransform: 'rotateX(70deg) rotateZ(0deg) scale(1)', duration: 50, particleFill: 'rgba(180, 200, 230, 0.6)', particleDelay: -1 },
|
||||
{ class: 'ring-2', initialTransform: 'rotateX(75deg) rotateZ(20deg) scale(0.7)', duration: 40, direction: 'reverse', particleFill: 'rgba(200, 180, 230, 0.5)', particleDelay: -2.5 },
|
||||
{ class: 'ring-3', initialTransform: 'rotateX(80deg) rotateZ(-15deg) scale(0.45)', duration: 60, particleFill: 'rgba(180, 230, 200, 0.55)', particleDelay: -0.5 },
|
||||
{ class: 'ring-4', initialTransform: 'rotateX(65deg) rotateZ(30deg) scale(1.2)', duration: 35, direction: 'reverse', particleFill: 'rgba(180, 200, 230, 0.6)', particleDelay: -3 }
|
||||
];
|
||||
|
||||
ringConfigs.forEach((config, ringIndex) => {
|
||||
const group = document.createElementNS(svgNS, "g");
|
||||
group.classList.add('stardust-ring');
|
||||
group.classList.add(config.class); // 应用CSS中定义的类
|
||||
group.style.transform = config.initialTransform; // 应用初始3D变换
|
||||
|
||||
// 应用Y轴旋转动画
|
||||
group.style.animationName = 'orbitYOnly'; // 使用只旋转Y轴的动画
|
||||
group.style.animationDuration = `${config.duration}s`;
|
||||
group.style.animationTimingFunction = 'linear';
|
||||
group.style.animationIterationCount = 'infinite';
|
||||
if (config.direction) {
|
||||
group.style.animationDirection = config.direction;
|
||||
}
|
||||
|
||||
const currentRingRadius = ringBaseRadius * (1 + ringIndex * 0.4 + Math.random()*0.2); // 半径随环增加
|
||||
|
||||
for (let i = 0; i < particlesPerRing; i++) {
|
||||
const angle = (i / particlesPerRing) * Math.PI * 2;
|
||||
const particle = document.createElementNS(svgNS, "circle");
|
||||
particle.classList.add('ring-particle');
|
||||
|
||||
const r = 0.8 + Math.random() * 0.7; // 粒子半径
|
||||
particle.setAttribute("r", r);
|
||||
particle.setAttribute("cx", Math.cos(angle) * currentRingRadius);
|
||||
particle.setAttribute("cy", Math.sin(angle) * currentRingRadius);
|
||||
|
||||
particle.style.fill = config.particleFill;
|
||||
// 粒子闪烁动画由CSS的.ring-particle和.ring-N .ring-particle控制
|
||||
// particle.style.animationDelay = `${config.particleDelay + Math.random() * 0.5 - 0.25}s`; // 为每个粒子稍微错开闪烁
|
||||
// 为了简化,让所有同环粒子用相同的闪烁延迟(由CSS :nth-child 或 config.particleDelay 控制)
|
||||
|
||||
group.appendChild(particle);
|
||||
}
|
||||
svg.appendChild(group);
|
||||
});
|
||||
|
||||
// CSS中已有的particleTwinkle动画
|
||||
// @keyframes particleTwinkle {
|
||||
// 0%, 100% { opacity: 0.5; transform: scale(0.8); }
|
||||
// 50% { opacity: 1; transform: scale(1.2); }
|
||||
// }
|
||||
// 将其加入style标签,如果它不在那里
|
||||
if (!document.querySelector("style[data-added-by-js='particleTwinkle']")) {
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.setAttribute("data-added-by-js", "particleTwinkle");
|
||||
styleSheet.innerHTML = `
|
||||
@keyframes particleTwinkle {
|
||||
0%, 100% { opacity: 0.4; transform: scale(0.7); }
|
||||
50% { opacity: 0.8; transform: scale(1.1); }
|
||||
}
|
||||
/* 如果需要,可以在这里动态添加.ring-N .ring-particle 的 animation-delay */
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<style>
|
||||
#svg-soft-halos-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #282c34; /* 柔和的深灰色 */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
#svg-soft-halos-background svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
}
|
||||
|
||||
.halo-source {
|
||||
/* 光晕源本身可以不可见,或者非常暗淡 */
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.rotating-shape {
|
||||
fill: rgba(200, 220, 255, 0.1); /* 非常淡的形状颜色 */
|
||||
stroke: rgba(200, 220, 255, 0.3);
|
||||
stroke-width: 1px;
|
||||
transform-origin: center center; /* 确保从中心旋转 */
|
||||
animation: slowRotate 30s linear infinite, pulseOpacity 15s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.shape-1 { transform: translate(25%, 30%) scale(0.8); animation-delay: -5s, -2s; }
|
||||
.shape-2 { transform: translate(70%, 60%) scale(1.1); animation-delay: -15s, -6s; }
|
||||
.shape-3 { transform: translate(50%, 75%) scale(0.6); animation-delay: -2s, -10s; }
|
||||
.shape-4 { transform: translate(15%, 80%) scale(0.9); animation-delay: -10s, -4s; }
|
||||
|
||||
|
||||
@keyframes slowRotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes pulseOpacity {
|
||||
0%, 100% { opacity: 0.3; filter: drop-shadow(0 0 5px rgba(180,200,255,0.2)); }
|
||||
50% { opacity: 0.7; filter: drop-shadow(0 0 15px rgba(200,220,255,0.4));}
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="svg-soft-halos-background">
|
||||
<svg viewBox="0 0 800 600" preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="25" result="coloredBlur"/> <!-- 调整stdDeviation控制模糊程度 -->
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/> <!-- 可选:将原始图形叠在模糊之上,如果需要更清晰的源 -->
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 光晕源 - 可以是几个大的圆形 -->
|
||||
<g filter="url(#softGlow)">
|
||||
<circle class="halo-source" cx="15%" cy="20%" r="150" fill="#5070A0" /> <!-- 淡蓝色光晕 -->
|
||||
<ellipse class="halo-source" cx="80%" cy="75%" rx="200" ry="120" fill="#7050A0" /> <!-- 淡紫色光晕 -->
|
||||
<circle class="halo-source" cx="50%" cy="50%" r="100" fill="#60A070" /> <!-- 淡绿色光晕 -->
|
||||
</g>
|
||||
|
||||
<!-- 缓慢旋转的几何图形 -->
|
||||
<g id="rotatingShapesLayer">
|
||||
<!-- 三角形 -->
|
||||
<polygon class="rotating-shape shape-1" points="0,-30 26,15 -26,15" />
|
||||
<!-- 方形 (旋转45度成菱形) -->
|
||||
<rect class="rotating-shape shape-2" x="-20" y="-20" width="40" height="40" transform="rotate(45)"/>
|
||||
<!-- 六边形 -->
|
||||
<polygon class="rotating-shape shape-3" points="30,0 15,26 -15,26 -30,0 -15,-26 15,-26" />
|
||||
<!-- 细长矩形 -->
|
||||
<rect class="rotating-shape shape-4" x="-40" y="-5" width="80" height="10" />
|
||||
</g>
|
||||
</svg>
|
||||
<script>
|
||||
// JS为几何图形添加更随机的初始位置和动画参数
|
||||
(function() {
|
||||
const shapes = document.querySelectorAll('#rotatingShapesLayer .rotating-shape');
|
||||
const svgViewBox = { width: 800, height: 600 }; // 从SVG viewBox获取或硬编码
|
||||
|
||||
shapes.forEach((shape, index) => {
|
||||
const scale = Math.random() * 0.6 + 0.5; // 0.5 to 1.1
|
||||
const posX = Math.random() * svgViewBox.width;
|
||||
const posY = Math.random() * svgViewBox.height;
|
||||
|
||||
// 移除CSS中的transform,由JS控制
|
||||
shape.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
|
||||
|
||||
const rotateDuration = Math.random() * 20 + 20; // 20s to 40s
|
||||
const opacityDuration = Math.random() * 10 + 10; // 10s to 20s
|
||||
const delayRotate = -(Math.random() * rotateDuration);
|
||||
const delayOpacity = -(Math.random() * opacityDuration);
|
||||
|
||||
shape.style.animation = `slowRotate ${rotateDuration}s linear ${delayRotate}s infinite, pulseOpacity ${opacityDuration}s ease-in-out ${delayOpacity}s infinite alternate`;
|
||||
|
||||
// 确保transform-origin在JS设置transform后依然有效
|
||||
// 对于SVG元素,transform-origin需要通过transform属性自身来模拟或依赖CSS。
|
||||
// 由于我们直接在JS中设置translate和scale, CSS的transform-origin: center center 会基于其初始0,0点。
|
||||
// 对于SVG <polygon> 和 <rect>, 它们的坐标是相对于自身(0,0)或x,y属性的。
|
||||
// CSS transform-origin center center 是针对HTML元素的。
|
||||
// SVG元素的旋转中心是其(0,0)点。如果图形不是以(0,0)为中心绘制的,需要调整。
|
||||
// 上面的图形都是以(0,0)为大致中心绘制的,所以默认旋转中心是OK的。
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,122 @@
|
||||
<style>
|
||||
#particle-network-container {
|
||||
width: 100%; height: 100%; background: #111; /* 深色背景 */
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
#particle-canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
<div id="particle-network-container">
|
||||
<canvas id="particle-canvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('particle-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer') || canvas.parentElement; // Fallback to parentElement if not in specific terminal
|
||||
if (!canvas || !layerElement) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width, height, particles = [];
|
||||
const particleCount = 80;
|
||||
const maxDistance = 100; // 最大连线距离
|
||||
const particleSpeed = 0.5;
|
||||
const particleColor = "rgba(173, 216, 230, 0.7)"; // 淡蓝色粒子
|
||||
const lineColor = "rgba(173, 216, 230, 0.2)"; // 淡蓝色连线
|
||||
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.x = Math.random() * width;
|
||||
this.y = Math.random() * height;
|
||||
this.vx = (Math.random() - 0.5) * particleSpeed * 2;
|
||||
this.vy = (Math.random() - 0.5) * particleSpeed * 2;
|
||||
this.radius = Math.random() * 2 + 1;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
if (this.x < 0 || this.x > width) this.vx *= -1;
|
||||
if (this.y < 0 || this.y > height) this.vy *= -1;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = particleColor;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
width = layerElement.offsetWidth;
|
||||
height = layerElement.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
particles = [];
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
}
|
||||
|
||||
function connectParticles() {
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < maxDistance) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.strokeStyle = lineColor;
|
||||
// 线条透明度随距离变化
|
||||
ctx.globalAlpha = 1 - (distance / maxDistance);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1; // Reset globalAlpha
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let animationFrameId;
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
particles.forEach(p => {
|
||||
p.update();
|
||||
p.draw();
|
||||
});
|
||||
connectParticles();
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
// Debounce resize function
|
||||
let resizeTimeout;
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
init();
|
||||
animate();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// Initialize and start animation
|
||||
init();
|
||||
animate();
|
||||
|
||||
// Listen for resize events to reinitialize
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
// Cleanup function for potential hot-reloading or component unmount
|
||||
canvas.cleanup = function() {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
window.removeEventListener('resize', onResize);
|
||||
console.log("Particle network cleaned up");
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<style>
|
||||
#elegant-sparkle-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #1c1c2e, #2a2a3f); /* 深邃蓝紫色渐变 */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sparkle {
|
||||
position: absolute;
|
||||
background-color: rgba(220, 220, 255, 0.8); /* 淡雅的亮白色/淡蓝色 */
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px rgba(200, 200, 255, 0.6),
|
||||
0 0 12px rgba(180, 180, 255, 0.4),
|
||||
0 0 20px rgba(150, 150, 255, 0.2);
|
||||
opacity: 0;
|
||||
animation: driftAndSparkle 15s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes driftAndSparkle {
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.5);
|
||||
}
|
||||
10%, 90% { /* 显现和消失的过程 */
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% { /* 主要可见状态 */
|
||||
opacity: 1;
|
||||
transform: translateY(-20px) scale(1.1);
|
||||
}
|
||||
20%, 80% { /* 中途的闪烁增强 */
|
||||
box-shadow: 0 0 10px rgba(220, 220, 255, 0.8),
|
||||
0 0 18px rgba(200, 200, 255, 0.6),
|
||||
0 0 30px rgba(180, 180, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 通过JS动态生成和定位sparkle元素效果更佳 */
|
||||
/* 这里用CSS nth-child做少量示例 */
|
||||
.sparkle:nth-child(1) { width: 3px; height: 3px; top: 15%; left: 20%; animation-duration: 18s; animation-delay: -2s; }
|
||||
.sparkle:nth-child(2) { width: 2px; height: 2px; top: 40%; left: 80%; animation-duration: 14s; animation-delay: -5s; }
|
||||
.sparkle:nth-child(3) { width: 4px; height: 4px; top: 70%; left: 50%; animation-duration: 20s; animation-delay: -8s; }
|
||||
.sparkle:nth-child(4) { width: 2px; height: 2px; top: 85%; left: 10%; animation-duration: 16s; animation-delay: -1s; }
|
||||
.sparkle:nth-child(5) { width: 3px; height: 3px; top: 5%; left: 60%; animation-duration: 22s; animation-delay: -12s; }
|
||||
.sparkle:nth-child(6) { width: 2px; height: 2px; top: 55%; left: 30%; animation-duration: 17s; animation-delay: -6s; }
|
||||
.sparkle:nth-child(7) { width: 4px; height: 4px; top: 30%; left: 90%; animation-duration: 19s; animation-delay: -9s; }
|
||||
.sparkle:nth-child(8) { width: 3px; height: 3px; top: 75%; left: 70%; animation-duration: 15s; animation-delay: -3s; }
|
||||
|
||||
</style>
|
||||
<div id="elegant-sparkle-background">
|
||||
<div class="sparkle"></div> <div class="sparkle"></div>
|
||||
<div class="sparkle"></div> <div class="sparkle"></div>
|
||||
<div class="sparkle"></div> <div class="sparkle"></div>
|
||||
<div class="sparkle"></div> <div class="sparkle"></div>
|
||||
<!-- 用JS生成更多,随机化参数 -->
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const container = document.getElementById('elegant-sparkle-background');
|
||||
if (!container) return;
|
||||
|
||||
const numSparkles = 25; // 控制光点数量
|
||||
const existingSparkles = container.querySelectorAll('.sparkle').length;
|
||||
|
||||
for (let i = 0; i < numSparkles - existingSparkles; i++) {
|
||||
const sparkle = document.createElement('div');
|
||||
sparkle.classList.add('sparkle');
|
||||
|
||||
const size = Math.random() * 2.5 + 1.5; // 1.5px to 4px
|
||||
sparkle.style.width = `${size}px`;
|
||||
sparkle.style.height = `${size}px`;
|
||||
|
||||
sparkle.style.top = `${Math.random() * 100}%`;
|
||||
sparkle.style.left = `${Math.random() * 100}%`;
|
||||
|
||||
const duration = Math.random() * 10 + 10; // 10s to 20s
|
||||
const delay = -(Math.random() * duration); // 立即开始,但相位不同
|
||||
sparkle.style.animationDuration = `${duration}s`;
|
||||
sparkle.style.animationDelay = `${delay}s`;
|
||||
|
||||
// 随机漂移方向(通过自定义属性)
|
||||
sparkle.style.setProperty('--drift-x', `${(Math.random() - 0.5) * 60}px`); // -30px to 30px
|
||||
sparkle.style.setProperty('--drift-y', `${(Math.random() - 0.5) * 60}px`); // -30px to 30px
|
||||
|
||||
container.appendChild(sparkle);
|
||||
}
|
||||
// 更新CSS @keyframes 来使用这些自定义属性
|
||||
// (或者在JS中直接修改transform,但CSS动画通常更流畅)
|
||||
// 由于直接在@keyframes中引用父元素(sparkle)的var较复杂,
|
||||
// 可以考虑每个sparkle元素内再套一个div,让这个内部div做transform动画
|
||||
// 或者,更简单的方法是让动画本身包含通用的漂移,通过animation-delay和duration差异化
|
||||
// 目前的 driftAndSparkle 动画是垂直漂移,可以通过多个keyframes或JS控制更复杂的漂移
|
||||
})();
|
||||
// 修正: 要让JS控制的随机漂移生效,需要修改@keyframes或采用不同策略。
|
||||
// 为简化,当前CSS动画已包含垂直漂移。JS主要用于生成更多粒子和随机化基础动画参数。
|
||||
</script>
|
||||
@@ -0,0 +1,219 @@
|
||||
<style>
|
||||
#floating-bubbles-container {
|
||||
width: 100%; height: 100%;
|
||||
background: radial-gradient(circle at center, #2c3e50, #1a2533); /* 深蓝灰色径向渐变 */
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
#bubbles-canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
<div id="floating-bubbles-container">
|
||||
<canvas id="bubbles-canvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('bubbles-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer') || canvas.parentElement;
|
||||
if (!canvas || !layerElement) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width, height, animationFrameId;
|
||||
const bubbles = [];
|
||||
const numBubbles = 10; // 少量大气泡
|
||||
const baseSpeed = 0.1;
|
||||
const baseRadius = 60;
|
||||
const radiusVariance = 40;
|
||||
|
||||
// Perlin noise function (simple implementation for distortion)
|
||||
// For more sophisticated noise, consider a library
|
||||
const ClassicalNoise = function(r) { // Classic Perlin noise
|
||||
if (r == undefined) r = Math;
|
||||
this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
|
||||
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
|
||||
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
|
||||
this.p = [];
|
||||
for (var i=0; i<256; i++) {
|
||||
this.p[i] = Math.floor(r.random()*256);
|
||||
}
|
||||
// To remove the need for index wrapping, double the permutation table length
|
||||
this.perm = [];
|
||||
for(var i=0; i<512; i++) {
|
||||
this.perm[i]=this.p[i&255];
|
||||
}
|
||||
};
|
||||
|
||||
ClassicalNoise.prototype.dot = function(g, x, y, z) {
|
||||
return g[0]*x + g[1]*y + g[2]*z;
|
||||
};
|
||||
|
||||
ClassicalNoise.prototype.mix = function(a, b, t) {
|
||||
return (1.0-t)*a + t*b;
|
||||
};
|
||||
|
||||
ClassicalNoise.prototype.fade = function(t) {
|
||||
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
||||
};
|
||||
|
||||
ClassicalNoise.prototype.noise = function(x, y, z) {
|
||||
// Find unit grid cell containing point
|
||||
var X = Math.floor(x);
|
||||
var Y = Math.floor(y);
|
||||
var Z = Math.floor(z);
|
||||
|
||||
// Get relative xyz coordinates of point within that cell
|
||||
x = x - X;
|
||||
y = y - Y;
|
||||
z = z - Z;
|
||||
|
||||
// Wrap the integer cells at 255 (smaller integer period can be introduced here)
|
||||
X = X & 255;
|
||||
Y = Y & 255;
|
||||
Z = Z & 255;
|
||||
|
||||
// Calculate a set of eight hashed gradient indices
|
||||
var gi000 = this.perm[X+this.perm[Y+this.perm[Z]]];
|
||||
var gi001 = this.perm[X+this.perm[Y+this.perm[Z+1]]];
|
||||
var gi010 = this.perm[X+this.perm[Y+1+this.perm[Z]]];
|
||||
var gi011 = this.perm[X+this.perm[Y+1+this.perm[Z+1]]];
|
||||
var gi100 = this.perm[X+1+this.perm[Y+this.perm[Z]]];
|
||||
var gi101 = this.perm[X+1+this.perm[Y+this.perm[Z+1]]];
|
||||
var gi110 = this.perm[X+1+this.perm[Y+1+this.perm[Z]]];
|
||||
var gi111 = this.perm[X+1+this.perm[Y+1+this.perm[Z+1]]];
|
||||
|
||||
// Calculate noise contributions from eight corners
|
||||
var n000= this.dot(this.grad3[gi000%12], x, y, z);
|
||||
var n100= this.dot(this.grad3[gi100%12], x-1, y, z);
|
||||
var n010= this.dot(this.grad3[gi010%12], x, y-1, z);
|
||||
var n110= this.dot(this.grad3[gi110%12], x-1, y-1, z);
|
||||
var n001= this.dot(this.grad3[gi001%12], x, y, z-1);
|
||||
var n101= this.dot(this.grad3[gi101%12], x-1, y, z-1);
|
||||
var n011= this.dot(this.grad3[gi011%12], x, y-1, z-1);
|
||||
var n111= this.dot(this.grad3[gi111%12], x-1, y-1, z-1);
|
||||
// Compute the fade curve value for x, y, z
|
||||
var u = this.fade(x);
|
||||
var v = this.fade(y);
|
||||
var w = this.fade(z);
|
||||
// Interpolate along x the contributions from each of the corners
|
||||
var nx00 = this.mix(n000, n100, u);
|
||||
var nx01 = this.mix(n001, n101, u);
|
||||
var nx10 = this.mix(n010, n110, u);
|
||||
var nx11 = this.mix(n011, n111, u);
|
||||
// Interpolate the four results along y
|
||||
var nxy0 = this.mix(nx00, nx10, v);
|
||||
var nxy1 = this.mix(nx01, nx11, v);
|
||||
// Interpolate the two last results along z
|
||||
var nxyz = this.mix(nxy0, nxy1, w);
|
||||
|
||||
return nxyz;
|
||||
};
|
||||
const perlin = new ClassicalNoise();
|
||||
let noiseTime = 0;
|
||||
|
||||
|
||||
class Bubble {
|
||||
constructor() {
|
||||
this.x = Math.random() * width;
|
||||
this.y = Math.random() * height;
|
||||
this.radius = baseRadius + Math.random() * radiusVariance;
|
||||
this.color1 = `rgba(${Math.floor(Math.random()*50+50)}, ${Math.floor(Math.random()*50+100)}, ${Math.floor(Math.random()*50+150)}, 0.1)`; // Blues/Greens
|
||||
this.color2 = `rgba(${Math.floor(Math.random()*50+100)}, ${Math.floor(Math.random()*50+50)}, ${Math.floor(Math.random()*50+120)}, 0.2)`; // Purples/Pinks
|
||||
|
||||
this.vx = (Math.random() - 0.5) * baseSpeed;
|
||||
this.vy = (Math.random() - 0.5) * baseSpeed;
|
||||
|
||||
this.numPoints = 30 + Math.floor(Math.random() * 20); // 组成气泡边缘的点数
|
||||
this.distortionFactor = 0.1 + Math.random() * 0.2; // 变形程度
|
||||
this.noiseSeedX = Math.random() * 1000;
|
||||
this.noiseSeedY = Math.random() * 1000;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
if (this.x - this.radius > width) this.x = -this.radius;
|
||||
if (this.x + this.radius < 0) this.x = width + this.radius;
|
||||
if (this.y - this.radius > height) this.y = -this.radius;
|
||||
if (this.y + this.radius < 0) this.y = height + this.radius;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
const points = [];
|
||||
for (let i = 0; i < this.numPoints; i++) {
|
||||
const angle = (i / this.numPoints) * Math.PI * 2;
|
||||
const noiseVal = perlin.noise(
|
||||
(Math.cos(angle) + 1) * 0.5 + this.noiseSeedX + noiseTime * 0.1, // x for noise
|
||||
(Math.sin(angle) + 1) * 0.5 + this.noiseSeedY + noiseTime * 0.1, // y for noise
|
||||
noiseTime * 0.2 // z for noise (time evolution)
|
||||
);
|
||||
const r = this.radius * (1 + noiseVal * this.distortionFactor);
|
||||
points.push({
|
||||
x: this.x + r * Math.cos(angle),
|
||||
y: this.y + r * Math.sin(angle)
|
||||
});
|
||||
}
|
||||
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 0; i < this.numPoints; i++) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % this.numPoints];
|
||||
const xc = (p1.x + p2.x) / 2;
|
||||
const yc = (p1.y + p2.y) / 2;
|
||||
ctx.quadraticCurveTo(p1.x, p1.y, xc, yc);
|
||||
}
|
||||
ctx.closePath();
|
||||
|
||||
const gradient = ctx.createRadialGradient(this.x, this.y, this.radius * 0.2, this.x, this.y, this.radius);
|
||||
gradient.addColorStop(0, this.color1);
|
||||
gradient.addColorStop(1, this.color2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.globalAlpha = 0.4 + Math.sin(noiseTime + this.noiseSeedX) * 0.2; // Subtle opacity pulse
|
||||
ctx.fill();
|
||||
|
||||
// Optional: subtle highlight
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x - this.radius * 0.3, this.y - this.radius * 0.3, this.radius * 0.3, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(200, 220, 255, 0.05)";
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
width = layerElement.offsetWidth;
|
||||
height = layerElement.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
bubbles.length = 0;
|
||||
for (let i = 0; i < numBubbles; i++) {
|
||||
bubbles.push(new Bubble());
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
noiseTime += 0.005;
|
||||
|
||||
bubbles.forEach(bubble => {
|
||||
bubble.update();
|
||||
bubble.draw();
|
||||
});
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
init();
|
||||
animate();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
init();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
canvas.cleanup = function() { if (animationFrameId) cancelAnimationFrame(animationFrameId); };
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<style>
|
||||
#css-gradient-mesh-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1a1a2e; /* 基础背景色 */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%; /* 圆形 */
|
||||
opacity: 0; /* 初始不可见,通过动画显现 */
|
||||
mix-blend-mode: screen; /* 或者 'overlay', 'soft-light' 尝试不同效果 */
|
||||
animation: flowAndFade 40s infinite ease-in-out;
|
||||
filter: blur(80px); /* 大量模糊制造柔和边缘 */
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 60vw; height: 60vw; /* 相对视窗大小 */
|
||||
top: -20%; left: -10%;
|
||||
background: radial-gradient(ellipse at center, #ff8a00 0%, transparent 70%); /* 橙色 */
|
||||
animation-delay: 0s;
|
||||
animation-duration: 45s;
|
||||
}
|
||||
.blob-2 {
|
||||
width: 50vw; height: 50vw;
|
||||
top: 10%; right: -15%;
|
||||
background: radial-gradient(ellipse at center, #e91e63 0%, transparent 70%); /* 粉色 */
|
||||
animation-delay: -10s;
|
||||
animation-duration: 50s;
|
||||
}
|
||||
.blob-3 {
|
||||
width: 70vw; height: 70vw;
|
||||
bottom: -25%; left: 20%;
|
||||
background: radial-gradient(ellipse at center, #00bcd4 0%, transparent 70%); /* 青色 */
|
||||
animation-delay: -20s;
|
||||
animation-duration: 55s;
|
||||
}
|
||||
.blob-4 {
|
||||
width: 55vw; height: 55vw;
|
||||
bottom: 5%; right: 5%;
|
||||
background: radial-gradient(ellipse at center, #8bc34a 0%, transparent 70%); /* 绿色 */
|
||||
animation-delay: -30s;
|
||||
animation-duration: 40s;
|
||||
}
|
||||
|
||||
@keyframes flowAndFade {
|
||||
0%, 100% {
|
||||
transform: translateX(0px) translateY(0px) scale(0.8);
|
||||
opacity: 0.3;
|
||||
}
|
||||
25% {
|
||||
transform: translateX(50px) translateY(-30px) scale(1.1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-40px) translateY(60px) scale(0.9);
|
||||
opacity: 0.5;
|
||||
}
|
||||
75% {
|
||||
transform: translateX(20px) translateY(-50px) scale(1.2);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div id="css-gradient-mesh-background">
|
||||
<div class="gradient-blob blob-1"></div>
|
||||
<div class="gradient-blob blob-2"></div>
|
||||
<div class="gradient-blob blob-3"></div>
|
||||
<div class="gradient-blob blob-4"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
<style>
|
||||
#canvas-stardust-container {
|
||||
width: 100%; height: 100%; background: #020208; /* 非常暗的蓝黑色 */
|
||||
overflow: hidden; position: relative;
|
||||
}
|
||||
#stardust-canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
<div id="canvas-stardust-container">
|
||||
<canvas id="stardust-canvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const canvas = document.getElementById('stardust-canvas');
|
||||
const layerElement = canvas.closest('.terminal-custom-html-layer') || canvas.parentElement;
|
||||
if (!canvas || !layerElement) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width, height, particles = [];
|
||||
const particleCount = 300; // 粒子数量,根据性能调整
|
||||
const baseSpeed = 0.3;
|
||||
const lifeSpan = 200; // 粒子生命周期(帧数)
|
||||
const baseRadius = 0.5;
|
||||
const radiusVariance = 1.5;
|
||||
const colors = ["#FFFFFF", "#DDDDFF", "#BBBBFF", "#9999EE"]; // 星尘颜色
|
||||
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.x = Math.random() * width;
|
||||
this.y = Math.random() * height; // 初始位置随机
|
||||
// 或者让粒子从一个方向进入
|
||||
// this.x = Math.random() * width;
|
||||
// this.y = height + Math.random() * 100; // 从底部进入
|
||||
|
||||
|
||||
this.vx = (Math.random() - 0.5) * baseSpeed * 0.5; // x方向轻微随机漂移
|
||||
this.vy = -baseSpeed - Math.random() * baseSpeed; // 主要向上流动
|
||||
|
||||
this.life = Math.random() * lifeSpan * 0.5 + lifeSpan * 0.5;
|
||||
this.initialLife = this.life;
|
||||
this.radius = baseRadius + Math.random() * radiusVariance;
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.life--;
|
||||
|
||||
if (this.life <= 0 || this.y < -this.radius || this.x < -this.radius || this.x > width + this.radius) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
// 透明度随生命周期变化,实现淡入淡出
|
||||
const blinkFactor = Math.sin( (this.initialLife - this.life) * 0.1 + Math.random() * Math.PI ); // 模拟闪烁
|
||||
const alpha = (this.life / this.initialLife) * 0.5 + 0.3 * blinkFactor;
|
||||
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
width = layerElement.offsetWidth;
|
||||
height = layerElement.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
particles = [];
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
}
|
||||
|
||||
let animationFrameId;
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.globalAlpha = 1; // Reset globalAlpha for clearing
|
||||
|
||||
particles.forEach(p => {
|
||||
p.update();
|
||||
p.draw();
|
||||
});
|
||||
ctx.globalAlpha = 1; // Reset globalAlpha after drawing all particles
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let resizeTimeout;
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
init();
|
||||
animate();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
init();
|
||||
animate();
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
canvas.cleanup = function() {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
window.removeEventListener('resize', onResize);
|
||||
console.log("Stardust canvas cleaned up");
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<style>
|
||||
|
||||
#static-grid-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #282c34;
|
||||
--grid-color-small: rgba(100, 100, 100, 0.1);
|
||||
--grid-size-small: 20px;
|
||||
--grid-color-large: rgba(120, 120, 120, 0.15);
|
||||
--grid-size-large: 100px;
|
||||
|
||||
background-image:
|
||||
linear-gradient(to right, var(--grid-color-small) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--grid-color-small) 1px, transparent 1px),
|
||||
linear-gradient(to right, var(--grid-color-large) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--grid-color-large) 1px, transparent 1px);
|
||||
|
||||
background-size:
|
||||
var(--grid-size-small) var(--grid-size-small),
|
||||
var(--grid-size-small) var(--grid-size-small),
|
||||
var(--grid-size-large) var(--grid-size-large),
|
||||
var(--grid-size-large) var(--grid-size-large);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="static-grid-background">
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user