前言
当我们鼠标在 win10 日历移动时,会有探照灯的显示效果,那么用前端如何实现呢?本文主要是对国外大神实现方式的理解与展示
日历基础视图
首先我们创建日历的基础视图
<h1>Windows 10 Calendar hover effect</h1>
<div class="win-grid">
<p class="week" id="1">Mo</p>
<p class="week" id="2">Tu</p>
<p class="week" id="3">We</p>
<p class="week" id="4">Th</p>
<p class="week" id="5">Fr</p>
<p class="week" id="6">Sa</p>
<p class="week" id="7">Su</p>
<div class="win-btn win-btn-inactive" id="40">29</div>
<div class="win-btn win-btn-inactive" id="41">30</div>
<div class="win-btn win-btn-inactive" id="42">31</div>
<div class="win-btn" id="1">1</div>
<div class="win-btn" id="2">2</div>
<div class="win-btn" id="3">3</div>
<div class="win-btn" id="4">4</div>
<div class="win-btn" id="5">5</div>
<div class="win-btn" id="6">6</div>
<div class="win-btn" id="7">7</div>
<div class="win-btn" id="8">8</div>
<div class="win-btn" id="9">9</div>
<div class="win-btn" id="10">10</div>
<div class="win-btn" id="11">11</div>
<div class="win-btn" id="12">12</div>
<div class="win-btn" id="13">13</div>
<div class="win-btn" id="14">14</div>
<div class="win-btn" id="15">15</div>
<div class="win-btn" id="16">16</div>
<div class="win-btn win-btn-active" id="17">17</div>
<div class="win-btn" id="18">18</div>
<div class="win-btn" id="19">19</div>
<div class="win-btn" id="20">20</div>
<div class="win-btn" id="21">21</div>
<div class="win-btn" id="22">22</div>
<div class="win-btn" id="23">23</div>
<div class="win-btn" id="24">24</div>
<div class="win-btn" id="25">25</div>
<div class="win-btn" id="26">26</div>
<div class="win-btn" id="27">27</div>
<div class="win-btn" id="28">28</div>
<div class="win-btn" id="29">29</div>
<div class="win-btn" id="30">30</div>
<div class="win-btn win-btn-inactive" id="31">1</div>
<div class="win-btn win-btn-inactive" id="32">2</div>
<div class="win-btn win-btn-inactive" id="33">3</div>
<div class="win-btn win-btn-inactive" id="34">4</div>
<div class="win-btn win-btn-inactive" id="35">5</div>
<div class="win-btn win-btn-inactive" id="36">6</div>
<div class="win-btn win-btn-inactive" id="37">7</div>
<div class="win-btn win-btn-inactive" id="38">8</div>
<div class="win-btn win-btn-inactive" id="39">9</div>
</div>
接着调整视图的基本样式,使用 grid
布局排列日期
* {
color: white;
text-transform: capitalize;
letter-spacing: 2px;
}
body {
background-color: black;
display: flex;
flex-flow: column wrap;
justify-content: center;
align-items: center;
}
.win-grid {
border: 1px solid white;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-gap: 0.2rem;
align-items: stretch;
text-align: center;
padding: 2rem;
cursor: default;
}
.win-btn {
padding: 1rem;
text-align: center;
border-radius: 0px;
border: 3px solid transparent;
background: black;
}
.win-btn-inactive {
color: #ffffff5f;
}
观察 win10 日期可以发现,如果当天日期被选中,日期背景会有一部分中空,日期的边框和其他部分是激活的颜色,这个如何实现呢
我们可以先设置边框的颜色,再使用background-origin设置元素背景相对区域
当天日期被选中时background-origin:content-box
, 选中其他日期时background-origin:border-box
.win-btn-active {
display: flex;
justify-content: center;
align-items: center;
padding: 0.2rem;
border: 3px solid red;
background: center linear-gradient(red, red) no-repeat;
background-origin: content-box;
}
.win-btn-active-unselected {
background-origin: border-box;
}
设置被选中元素的边框样式,以及鼠标悬浮在当前日期和被选择日期时的样式
.win-btn-selected {
border: 3px solid red;
}
.win-btn-active:hover {
border: 3px solid hsl(0, 90%, 75%);
}
.win-btn-selected:hover {
border: 3px solid hsl(0, 70%, 50%) !important;
}
探照灯效果实现
日期选择状态切换
首先需要保存选择日期对象,监听日期点击事件,保存最后一个选择的日期对象,其次要根据选中状态,动态切换当前日期样式
let activeBtn = document.querySelector(".win-btn-active");
let lastClicked = null;
document.querySelectorAll(".win-btn").forEach((btn) => {
btn.onclick = (e) => {
if (lastClicked) {
lastClicked.classList.remove("win-btn-selected");
}
lastClicked = e.currentTarget;
activeBtn.classList.toggle(
"win-btn-active-unselected",
e.currentTarget.id !== activeBtn.id
);
e.currentTarget.classList.add("win-btn-selected");
};
});
悬浮日期边框高亮
要实现控制元素边框显示效果,需要用到border-image
属性,我们需要为每个日期添加鼠标移动事件,计算鼠标相对日期元素的坐标,然后为元素添加border-image
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
e.target.style.borderImage = `radial-gradient(25% 75% at ${x}px ${y}px, rgba(245,7,181,0.7),rgba(245,7,181,0.1)) 1 / 2px / 0px stretch`;
});
临近日期边框高亮
如何让鼠标附近的日期元素也高亮现实呢?我们可以以当前位置为原点,画一个圆形,在圆形上标记出几个点,检测点所在的位置,如果所在位置存在日期元素,就高亮该元素边框,这里需要用到elementFromPoint方法
取圆上的 8 个点
计算各个点的坐标需要用到三角函数
// 设置圆的半径
const offset = 69;
const angles = [];
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
let nearBy = [];
body.addEventListener("mousemove", (e) => {
const x = e.clientX;
const y = e.clientY;
nearBy = angles.reduce((acc, rad, index, 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 &&
element.classList.contains("win-btn") &&
!element.classList.contains("win-btn-active") &&
!element.classList.contains("win-btn-selected") &&
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}px ${offset}px at ${bx}px ${by}px ,rgba(245,7,181,0.7),rgba(245,7,181,0.1),transparent ) 1 / 3px / 0px stretch `;
return [...acc, element];
}
return acc;
}, []);
});
上面代码实现了鼠标周边日期元素边框高亮,但存在一个问题,如果鼠标移动到日期元素上面,当前日期元素边框没有正常高亮,为什么有这个问题呢
这跟设置的圆的半径有关,辐射范围过大,导致没有点(设置的 8 个点)在当前元素上面,我们可以在每个点和原点连线线上面,再取一个靠中心的点,获取当前元素
body.addEventListener("mousemove", (e) => {
const x = e.clientX;
const y = e.clientY;
nearBy = angles.reduce((acc, rad, index, arr) => {
const offsets = [offset * 0.35, offset];
const elements = offsets.reduce((elementAccumulator, o, i, offsetArray) => {
const cx = Math.floor(x + Math.cos(rad) * o);
const cy = Math.floor(y + Math.sin(rad) * o);
const element = document.elementFromPoint(cx, cy);
if (
element &&
element.classList.contains("win-btn") &&
!element.classList.contains("win-btn-active") &&
!element.classList.contains("win-btn-selected") &&
elementAccumulator.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}px ${offset}px at ${bx}px ${by}px ,rgba(245,7,181,0.7),rgba(245,7,181,0.1),transparent ) 1 / ${borderWidth}px / 0px stretch `;
return [...elementAccumulator, element];
}
return elementAccumulator;
}, []);
return acc.concat(elements);
}, []);
});
每次鼠标移动都要重新计算,因此要清除 nearBy
中的数据
function clearNearBy() {
nearBy.splice(0).forEach((e) => (e.style.borderImage = null));
}
body.addEventListener("mousemove", (e) => {
clearNearBy();
});
body.onmouseleave = (e) => {
clearNearBy();
};
总结
要实现探照灯效果,首先需要弄明白border-image的使用,其次是如何获得以鼠标为中心,周边附近元素的位置信息,这里用到的关键方法是elementFromPoint
源码
windows 10 calendar hover effect
参考
Windows 10 calendar hover effect using HTML, CSS, and vanilla JS