使用 HTML、CSS 和 vanilla JS 实现 Windows 10 网格悬停效果
让我们开始吧!
这就是全部代码
目录
注意:撰写本文的目的是让所有技能水平的读者都能理解尽可能多的内容。我已经在本文中简要解释了该效果中使用的所有必要基本概念;所以请不要因为篇幅过长而忽略本文。如果您不是初学者,我建议您仔细阅读内容并提供宝贵的反馈意见 :)
介绍
大家好,如果您是读了我上一篇文章之后才来到这里的,那么恭喜您,因为您已经理解了这个效果👏中一半的代码了。我强烈建议您阅读第一部分(按钮悬停效果),因为我会解释所有这些效果中使用的一些基本 CSS 属性。
您可以在下面查看最终的网格悬停效果。
让我们开始吧!
观察
- 光标移动到某个网格项附近。
- 一旦它与物品的距离达到最小值,附近物品的边框就会突出显示。
- 项目边框上的突出显示强度取决于光标的位置。
因此,很明显我们将处理鼠标事件,尤其是mousemove
事件。
入门
我开始进行基本设置,首先从Codepen 中 fork 了我自己实现的 Windows 按钮悬停效果,然后将鼠标事件添加到win-grid
元素中。以下是初始代码。
HTML
<html>
<head>
<title>Windows 10 grid hover effect</title>
</head>
<body>
<h1>Windows 10 Button & Grid Hover Effect</h1>
<div class="win-grid">
<div class="win-btn" id="1">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="2">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="3">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="4">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="5">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="6">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="7">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="8">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="9">This is a windows hoverable item inside windows grid</div>
</div>
</body>
</html>
CSS
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100&display=swap");
* {
box-sizing: border-box;
color: white;
font-family: "Noto Sans JP", sans-serif;
}
body {
background-color: black;
display: flex;
flex-flow: column wrap;
justofy-content: center;
align-items: center;
}
.win-grid {
border: 1px solid white;
letter-spacing: 2px;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: stretch;
text-align: center;
grid-gap: 1rem;
padding: 5rem;
}
.win-btn {
padding: 1rem 2rem;
text-align: center;
border: none;
border-radius: 0px;
border: 1px solid transparent;
}
button:focus {
outline: none;
}
JS
document.querySelectorAll(".win-btn").forEach((b) => {
b.onmouseleave = (e) => {
e.target.style.background = "black";
e.target.style.borderImage = null;
};
b.addEventListener("mousemove", (e) => {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
const y = e.clientY - rect.top; //y position within the element.
e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.2),rgba(255,255,255,0) )`;
e.target.style.borderImage = `radial-gradient(20% 75% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 1 / 1px / 0px stretch `;
});
});
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
//effect logic here
});
这就是我们现在的输出结果

对上述代码进行简单解释:
HTML代码非常简单,一个容器 div 用作网格,里面是项目。
在CSS中,我使用 CSS
网格来布局项目,以便设计保持响应式。网格布局包含 3 个项目,网格的类名为 win-grid,网格项目类名为 win-btn。JS是按钮悬停效果的代码。有关详细说明,请阅读此处。
现在开始有趣的部分!
关键
注意:这是我的逻辑,可以采用不同的方法来实现这种效果,但在查看了网上现有的实现之后,我可以向您保证,我的方法是干净的,最简单的,并且可扩展的,不像其他硬编码的方法😉。
当光标进入网格区域时,我们需要在光标周围放置元素,使其距离达到特定值。我offset
在代码中引用了这个半径或距离值。坏消息是,JS 中没有方法可以查找特定区域内的元素,但好消息是,有一种方法可以通过给定坐标来查找元素!
该方法为document.elementFromPoint(x,y)
;
它返回位于作为参数传递的坐标之下的最顶层元素。因此,如果坐标有效,则该方法将返回body
或 内的其他元素body
。
您立即会问,我们究竟如何使用这种方法来查找周围附近的元素以及我们传递什么坐标?
为了理解这一点,请看下文。
查找光标附近的元素
从图中,你可能猜到我们将检查圆形区域圆周上的点。完全正确!
我们有两种方法:
- 我们要么检查圆周上的所有点
- 我们跳过了一些要点
显然,选项 2 看起来没那么复杂;但是要检查哪些点,又要跳过哪些点呢?
由于网格内(靠近光标)元素的最大数量为 4,因此我们可以像在现实生活中一样,检查光标周围的所有 8 个方向!
如何计算邻近点
由于这些点位于圆的圆周上,我们将使用简单的向量数学来找到它们。
因此,假设p(x,y)是圆周上的一个点,位于原点,半径为r ,与X 轴成特定角度,则其坐标计算如下:
px = r*cos(angle)
py = r*sin(angle)
注意:角度以弧度为单位,即(度*PI/180)
你可以直接计算这些点,用简单的逻辑(x-offset,y)表示左侧,(x+offset,y)表示右侧,以此类推……但这会造成太多的硬编码。最初,我尝试了这种方法,但后来意识到,如果我想增加或减少光标位置周围的点数,就必须声明或注释掉几行代码,这样一来,我们编写的代码效率就不高了 🙃
由于光标不会位于原点,我们需要将距离原点的 x 和 y 距离添加到坐标 px 和 py 上(参见上图)。因此,圆周上点的新坐标变为 cx,cy (我称之为改变 x 和 y)。
所以公式变为
cx = x + r*cos(angle)
cy = y + r*sin(angle)
//where x,y refers to the current position of the cursor on the screen
ℹ:屏幕原点在左上角,左边缘为Y轴正方向,上边缘为X轴正方向。
选择并设计正确的元素
现在,既然我们知道如何找到这 8 个点,我们就在这些点上找到元素。我们先检查元素是否为空,然后检查它的类是否为win-btn
空,此外,我们还需要检查元素是否已存在于数组中nearBy
。只有当元素不存在于数组中时,我们才会继续处理该元素nearBy
;最后,我们才会应用border-image
到该元素上。
为什么不先保存元素,然后再循环遍历数组呢……说实话,那样做起来太麻烦了。
需要检查数组中是否存在
nearBy
,因为每次鼠标移动时都会触发 mouseover 事件,而我们的逻辑也会在每次事件触发时被触发。因此,我们需要确保不会重复保存相同的元素。
现在,边界图像的计算已经在前面的文章中解释过了,所以我就不再在这里解释。
如果上述解释对您来说没有意义,请查看下面的代码。
有些读者此时会喜欢
给你😜
代码
//generate the angle values in radians
const angles = [];
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
//for each angle, find and save elements at that point
let nearBy = [];
nearBy = angles.reduce((acc, rad, i, arr) => {
//find the coordinate for current angle
const cx = Math.floor(x + Math.cos(rad) * offset);
const cy = Math.floor(y + Math.sin(rad) * offset);
const element = document.elementFromPoint(cx, cy);
if (element !== null) {
;
if (
element.className === "win-btn" &&
acc.findIndex((ae) => ae.id === element.id) < 0
) {
const brect = element.getBoundingClientRect();
const bx = x - brect.left; //x position within the element.
const by = y - brect.top; //y position within the element.
if (!element.style.borderImage)
element.style.borderImage = `radial-gradient(${offset * 2}px ${
offset * 2
}px at ${bx}px ${by}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1),transparent ) 9 / 1px / 0px stretch `;
return [...acc, element];
}
}
return acc;
}, []);
- *这是什么代码?*🥴
- *他为什么用
reduce()
,为什么不用map()
?forEach()
*🤔 - 这是什么
reduce()
方法? 😓
想想我们要遵循哪些步骤……
对于数组中的每个角度angles
,
1. 我们要从坐标中找到一个元素。2
. 如果有效,则将样式应用于元素
3. 将应用样式的元素保存到nearBy
数组中
因此,在处理完数组的每个角度之后angle
,我们想要一个结果,即一个包含所有 nearBy 元素的数组,然后将其存储在nearBy
数组中。
在这种情况下,我们希望对数组的每个项目执行某些操作后得到单个输出,我们使用该reduce()
方法。
Reduce 方法
它需要 2 个参数
- 对数组中的每个项目执行的函数,通过对先前的结果执行某些操作来返回更新的结果。
- 变量(通常称为累加器),等于上述函数返回的最新结果
第一个参数即函数
这有几个论点
- 累加器(这将是当前项目的结果)
- 数组的当前项
- 项目的索引(可选参数)
- 我们正在循环的数组本身(可选参数)
那么,reduce 内部发生的事情是
- 它从角度数组的第一项开始。累加器具有我们在代码中设置的初始值(在我们的例子中,它是一个空数组)。当前索引为 0,在函数内部,我们根据当前角度找到一个元素并对其应用 CSS(如果适用),最后我们返回一个新数组,其中包含累加器中现有的项(由于累加器为空,这些项目前不存在),新元素设为e1
[...acc, element]
。
因此我们更新后的累加器是[e1]
- 现在,对于数组中的第二项,此过程重复,因此累加器变为
[e1,e2]
- 如此循环,直到到达数组末尾。4.假设我们得到一个元素e3 ,它
win-grid
本身就是 e1,我们不想将它添加到 e2accumulator
,因此我们直接返回 e1accumulator
,e2 的值。因此我们的累加器仍然只是 [e1,e2]。
我们为什么不使用map()
或forEach()
原因有二
- 如果我们在函数中不返回任何内容
map
,它将undefined
在结果数组中保存一个值,要删除这些值,我们必须使用该filter()
方法🥴,我们不想仅仅为了这个而重复数组。 - forEach 方法不返回任何值,它将为每个项目运行一个函数,我们必须手动将项目推送到
nearby
数组中,这并不不正确,但该reduce()
方法存在于此类用例中,因此在这里使用更合适reduce()
。
太多了!!!
让我们看一下此时的代码和输出。
const offset = 69;
const angles = []; //in deg
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
let nearBy = [];
/*Effect #1 - https://codepen.io/struct_dhancha/pen/QWdqMLZ*/
document.querySelectorAll(".win-btn").forEach((b) => {
b.onmouseleave = (e) => {
e.target.style.background = "black";
e.target.style.borderImage = null;
e.target.border = "1px solid transparent";
};
b.addEventListener("mousemove", (e) => {
e.stopPropagation();
e.target.border = "1px solid transparent";
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
const y = e.clientY - rect.top; //y position within the element.
e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.25),rgba(255,255,255,0) )`;
e.target.style.borderImage = `radial-gradient(20% 65% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 9 / 2px / 0px stretch `;
});
});
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
const x = e.x; //x position within the element.
const y = e.y; //y position within the element.
nearBy = angles.reduce((acc, rad, i, arr) => {
const cx = Math.floor(x + Math.cos(rad) * offset);
const cy = Math.floor(y + Math.sin(rad) * offset);
const element = document.elementFromPoint(cx, cy);
if (element !== null) {
if (
element.className === "win-btn" &&
acc.findIndex((ae) => ae.id === element.id) < 0
) {
const brect = element.getBoundingClientRect();
const bx = x - brect.left; //x position within the element.
const by = y - brect.top; //y position within the element.
if (!element.style.borderImage)
element.style.borderImage = `radial-gradient(${offset * 2}px ${
offset * 2
}px at ${bx}px ${by}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1),transparent ) 9 / 1px / 0px stretch `;
return [...acc, element];
}
}
return acc;
}, []);
});
这是输出
如你所见,我们成功检测并高亮了附近的元素🎉。
但是,我们一定不要忘记在鼠标移动时清除之前应用的效果。这样,每次鼠标移动时,之前位置高亮的元素都会变回其原始的透明边框状态,然后我们会重新计算所有附近的元素,并将效果应用于有效的元素!对了,别忘了清除之前保存的 nearBy 元素,否则你的光标会移动到新的位置,当前的 nearBy 和之前的 nearBy 元素都会被高亮 😂 这会让人很不爽。
所以有两件事要做:移除所有 nearBy 元素及其 border-image。我们在计算新的 nearBy 元素之前执行此操作。
//inside the event listener
nearBy.splice(0, nearBy.length).forEach((e) => (e.style.borderImage = null));
//reduce method below
这行代码完成了我说的两件事。
该splice()
方法接受一个起始索引以及要从该起始索引处移除的项目数(包括起始索引),并修改原始数组。因此,在 splice() 操作之后,我们的nearBy
数组为空。该splice()
方法返回一个包含所有被移除项目的数组。因此,我们迭代该数组并移除border-image
所有这些元素!
我们快完成了...
处理边缘情况
仅涉及一些小的边缘情况......
- 此外,当我们输入按钮时,我们希望清除应用于按钮的任何现有网格效果
- 光标离开时清除所有效果
win-grid
对于情况 1,
在发生的情况下清除nearBy
数组!mouseenter
win-btn
对于情况 2,
在发生的情况下清除nearBy
数组!mouseleave
win-grid
由于附近的清理操作需要多次执行,因此我已将该代码转移到一个方法中clearNearBy()
,并在需要进行清理的任何地方调用该方法。
这就是全部代码
const offset = 69;
const angles = []; //in deg
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
let nearBy = [];
function clearNearBy() {
nearBy.splice(0, nearBy.length).forEach((e) => (e.style.borderImage = null));
}
/*Effect #1 - https://codepen.io/struct_dhancha/pen/QWdqMLZ*/
document.querySelectorAll(".win-btn").forEach((b) => {
b.onmouseleave = (e) => {
e.target.style.background = "black";
e.target.style.borderImage = null;
e.target.border = "1px solid transparent";
};
b.onmouseenter = (e) => {
clearNearBy();
};
b.addEventListener("mousemove", (e) => {
e.target.border = "1px solid transparent";
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
const y = e.clientY - rect.top; //y position within the element.
e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.25),rgba(255,255,255,0) )`;
e.target.style.borderImage = `radial-gradient(20% 65% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 9 / 2px / 0px stretch `;
});
});
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
const x = e.x; //x position within the element.
const y = e.y; //y position within the element.
clearNearBy();
nearBy = angles.reduce((acc, rad, i, arr) => {
const cx = Math.floor(x + Math.cos(rad) * offset);
const cy = Math.floor(y + Math.sin(rad) * offset);
const element = document.elementFromPoint(cx, cy);
if (element !== null) {
if (
element.className === "win-btn" &&
acc.findIndex((ae) => ae.id === element.id) < 0
) {
const brect = element.getBoundingClientRect();
const bx = x - brect.left; //x position within the element.
const by = y - brect.top; //y position within the element.
if (!element.style.borderImage)
element.style.borderImage = `radial-gradient(${offset * 2}px ${
offset * 2
}px at ${bx}px ${by}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1),transparent ) 9 / 1px / 0px stretch `;
return [...acc, element];
}
}
return acc;
}, []);
});
body.onmouseleave = (e) => {
clearNearBy();
};
如果您已经到达这里,那么非常感谢🙏您完成这篇文章。
如果您有任何疑问或问题,请随时发表评论,我会尽力帮助您!😁
准备好迎接我的下一篇文章吧,它将讲解如何使用我在这两篇文章中讲解的概念来创建 Windows 10 日历效果。
别忘了分享这篇文章给你的开发者朋友们😉。
其他资源
您可以参考下面提到的附加资源,以更好地理解 CSS 和 JS。
鏂囩珷鏉ユ簮锛�https://dev.to/jashgopani/windows-10-grid-hover-effect-using-html-css-and-vanilla-js-42d9