257 lines
12 KiB
HTML
257 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>标签气泡动效</title>
|
|
<style></style>
|
|
|
|
<link rel="stylesheet" href="./static/css/song-request-station.css" />
|
|
</head>
|
|
<body>
|
|
<!-- <div class="container" id="bubbleContainer"></div> -->
|
|
<div class="container">
|
|
<div class="container-box mar1200">
|
|
<img class="logo" src="./static/img/logo.png" alt="" />
|
|
<div class="header">
|
|
<img class="halo" src="./static/img/halo.png" />
|
|
<img class="record-black" src="./static/img/record-black.svg" />
|
|
<div class="record-circle"></div>
|
|
<img class="star-icon" src="./static/img/star-icon.png" alt="" />
|
|
<img class="bj-2" src="./static/img/song-request-bj-2.svg" alt="" />
|
|
<img class="love-little" src="./static/img/love-little.svg" alt="" />
|
|
<img class="music-icon" src="./static/img/music-icon.svg" alt="" />
|
|
<img class="bj" src="./static/img/song-request-bj.svg" alt="" />
|
|
<img class="love-big" src="./static/img/love-big.svg" alt="" />
|
|
<img class="music-score" src="./static/img/music-score.png" />
|
|
<img class="robot" src="./static/img/robot.png" />
|
|
<img class="text" src="./static/img/song-request-text.svg" />
|
|
<img class="face" src="./static/img/smiling-face.png" />
|
|
<img class="star-icon-2" src="./static/img/star-icon-2.png" />
|
|
<img class="ai-music" src="./static/img/ai-music.png" />
|
|
<img class="green-glow" src="./static/img/green-glow.png" />
|
|
<img class="shadow" src="./static/img/shadow.png" />
|
|
</div>
|
|
<div class="list-box">
|
|
<div class="list" id="bubbleContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 标签数据
|
|
const tags = ["前端开发", "JavaScript", "CSS动画", "HTML5", "React", "Vue", "TypeScript", "Node.js", "UI设计", "用户体验", "响应式布局", "性能优化", "微信小程序", "PWA", "Canvas", "SVG", "WebGL", "数据可视化", "模块化", "组件化", "", "", "", "", ""];
|
|
|
|
// 获取容器
|
|
const container = document.getElementById("bubbleContainer");
|
|
|
|
// 创建空白标签数组,用于存储所有空白标签的信息
|
|
const emptyTags = [];
|
|
|
|
// 存储所有标签的位置和大小信息
|
|
const allTags = [];
|
|
const tagSizes = []; // 存储每个标签的实际尺寸
|
|
const defaultTagWidth = 120; // 默认标签宽度
|
|
const defaultTagHeight = 40; // 默认标签高度
|
|
|
|
// 计算容器尺寸
|
|
function getContainerDimensions() {
|
|
const containerWidth = container.offsetWidth;
|
|
const containerHeight = container.offsetHeight;
|
|
return { containerWidth, containerHeight };
|
|
}
|
|
|
|
// 获取标签的实际尺寸
|
|
function getTagDimensions(tagText) {
|
|
// 创建一个临时标签来测量实际尺寸
|
|
const tempTag = document.createElement("div");
|
|
tempTag.className = "bubble-tag";
|
|
tempTag.textContent = tagText;
|
|
tempTag.style.position = "absolute";
|
|
tempTag.style.visibility = "hidden";
|
|
tempTag.style.left = "-9999px";
|
|
tempTag.style.top = "-9999px";
|
|
|
|
document.body.appendChild(tempTag);
|
|
const width = tempTag.offsetWidth;
|
|
const height = tempTag.offsetHeight;
|
|
document.body.removeChild(tempTag);
|
|
|
|
// 确保至少有默认的尺寸
|
|
return {
|
|
width: Math.max(width, defaultTagWidth),
|
|
height: Math.max(height, defaultTagHeight),
|
|
};
|
|
}
|
|
|
|
// 检查两个标签是否碰撞
|
|
function isColliding(tag1, tag2, padding = 20) {
|
|
// 计算两个标签之间的距离
|
|
const dx = Math.abs(tag1.x + tag1.width / 2 - (tag2.x + tag2.width / 2));
|
|
const dy = Math.abs(tag1.y + tag1.height / 2 - (tag2.y + tag2.height / 2));
|
|
|
|
// 如果距离小于两个标签半径之和加上内边距,则发生碰撞
|
|
return dx < tag1.width / 2 + tag2.width / 2 + padding && dy < tag1.height / 2 + tag2.height / 2 + padding;
|
|
}
|
|
|
|
// 计算理想的网格大小以实现均匀分布
|
|
function calculateGridSize(totalTags, containerWidth, containerHeight, avgTagSize) {
|
|
// 估计每个标签需要的空间(包括间距)
|
|
const tagSpacing = avgTagSize + 60; // 标签直径 + 间距
|
|
|
|
// 计算大致的行数和列数
|
|
const idealColumns = Math.ceil(Math.sqrt((totalTags * containerWidth) / containerHeight));
|
|
const idealRows = Math.ceil(totalTags / idealColumns);
|
|
|
|
// 确保网格不会太拥挤
|
|
const actualColumns = Math.min(idealColumns, Math.floor(containerWidth / tagSpacing));
|
|
const actualRows = Math.min(idealRows, Math.floor(containerHeight / tagSpacing));
|
|
|
|
return { columns: Math.max(1, actualColumns), rows: Math.max(1, actualRows) };
|
|
}
|
|
|
|
// 为新标签找到一个不与其他标签碰撞的位置,优先使用均匀分布
|
|
function findNonCollidingPosition(tagWidth, tagHeight, containerWidth, containerHeight, totalTags, currentIndex) {
|
|
const maxAttempts = 200;
|
|
let attempts = 0;
|
|
|
|
// 计算网格大小
|
|
const avgTagSize = (tagWidth + tagHeight) / 2;
|
|
const { columns, rows } = calculateGridSize(totalTags, containerWidth, containerHeight, avgTagSize);
|
|
|
|
// 首先尝试网格均匀分布位置
|
|
while (attempts < maxAttempts * 0.7) {
|
|
attempts++;
|
|
|
|
// 计算理想的网格位置
|
|
const gridX = (currentIndex % columns) * (containerWidth / columns);
|
|
const gridY = Math.floor(currentIndex / columns) * (containerHeight / rows);
|
|
|
|
// 添加一些随机偏移,避免完全规则排列,但保持大致均匀
|
|
const randomOffset = 0.2; // 20%的随机偏移
|
|
const xOffset = (((Math.random() - 0.5) * containerWidth) / columns) * randomOffset;
|
|
const yOffset = (((Math.random() - 0.5) * containerHeight) / rows) * randomOffset;
|
|
|
|
// 计算最终位置,确保不会超出容器
|
|
const x = Math.max(0, Math.min(containerWidth - tagWidth, gridX + xOffset));
|
|
const y = Math.max(0, Math.min(containerHeight - tagHeight, gridY + yOffset));
|
|
|
|
const newTag = { x, y, width: tagWidth, height: tagHeight };
|
|
|
|
// 检查与所有已有标签是否碰撞
|
|
let colliding = false;
|
|
for (let i = 0; i < allTags.length; i++) {
|
|
if (isColliding(newTag, allTags[i])) {
|
|
colliding = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!colliding) {
|
|
return { x, y };
|
|
}
|
|
}
|
|
|
|
// 如果网格分布尝试失败,回退到随机位置搜索
|
|
while (attempts < maxAttempts) {
|
|
attempts++;
|
|
|
|
// 随机生成一个位置
|
|
const x = Math.random() * (containerWidth - tagWidth);
|
|
const y = Math.random() * (containerHeight - tagHeight);
|
|
const newTag = { x, y, width: tagWidth, height: tagHeight };
|
|
|
|
// 检查与所有已有标签是否碰撞
|
|
let colliding = false;
|
|
for (let i = 0; i < allTags.length; i++) {
|
|
if (isColliding(newTag, allTags[i])) {
|
|
colliding = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!colliding) {
|
|
return { x, y };
|
|
}
|
|
}
|
|
|
|
// 如果尝试次数用完仍然找不到完全不碰撞的位置,返回随机位置
|
|
return {
|
|
x: Math.random() * (containerWidth - tagWidth),
|
|
y: Math.random() * (containerHeight - tagHeight),
|
|
};
|
|
}
|
|
|
|
// 创建标签并添加动画
|
|
function createTags() {
|
|
const { containerWidth, containerHeight } = getContainerDimensions();
|
|
const totalTags = tags.length;
|
|
|
|
// 清空容器
|
|
container.innerHTML = "";
|
|
allTags.length = 0;
|
|
tagSizes.length = 0;
|
|
|
|
tags.forEach((tagText, index) => {
|
|
// 创建标签元素
|
|
const tag = document.createElement("div");
|
|
tag.className = "bubble-tag";
|
|
tag.textContent = tagText;
|
|
|
|
// 获取标签的实际尺寸
|
|
const { width, height } = getTagDimensions(tagText);
|
|
tagSizes.push({ width, height });
|
|
|
|
// 查找不碰撞的位置
|
|
const { x, y } = findNonCollidingPosition(width, height, containerWidth, containerHeight, totalTags, index);
|
|
tag.style.left = `${x}px`;
|
|
tag.style.top = `${y}px`;
|
|
|
|
// 保存标签信息
|
|
allTags.push({ x, y, width, height, element: tag });
|
|
|
|
// 随机大小
|
|
const randomSize = 0.9 + Math.random() * 0.4; // 稍微增大了最小尺寸
|
|
tag.style.transform = `scale(${randomSize})`;
|
|
|
|
// 随机动画延迟
|
|
const delay = Math.random() * 5;
|
|
tag.style.animationDelay = `${delay}s`;
|
|
|
|
// 随机动画持续时间
|
|
const duration = 10 + Math.random() * 10; // 延长动画周期,使效果更流畅
|
|
tag.style.animation = `float ${duration}s infinite ease-in-out, pulse 3s infinite alternate`;
|
|
|
|
// 随机背景色 - 模仿截图中的颜色范围
|
|
const hue = 250 + Math.random() * 60; // 紫色到粉色的范围
|
|
const lightness = 85 + Math.random() * 10; // 亮度调整
|
|
tag.style.backgroundColor = `hsla(${hue}, 70%, ${lightness}%, 0.9)`;
|
|
|
|
// 添加到容器
|
|
container.appendChild(tag);
|
|
|
|
// 添加点击效果
|
|
tag.addEventListener("click", () => {
|
|
tag.style.animation = "none";
|
|
tag.style.transform = "scale(1.3)";
|
|
tag.style.zIndex = "10";
|
|
setTimeout(() => {
|
|
tag.style.animation = `float ${duration}s infinite ease-in-out, pulse 3s infinite alternate`;
|
|
tag.style.animationDelay = "0s";
|
|
tag.style.zIndex = "1";
|
|
}, 300);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 窗口大小变化时重新创建标签
|
|
window.addEventListener("resize", () => {
|
|
createTags();
|
|
});
|
|
|
|
// 初始创建标签
|
|
createTags();
|
|
</script>
|
|
</body>
|
|
</html>
|