Эффект "фонарика" на календаре как в Win10

В Win10 в календаре есть эффект с плавным изменением цвета границ ячеек календаря, при движении мышкой получается как-будто "фонарик" подсвечивает ячейки. Скрины:

введите сюда описание изображения введите сюда описание изображения

Мне стало интересно реализовать это на css/js, но с наскоку как-то не получилось) Как можно это сделать?

Вот заготовка:

const cellTemplate = document.querySelector('#cell-template').content.querySelector('.cell');
const list = document.querySelector('.list');

for (let i = 0; i < 30; i++) {
  const cell = cellTemplate.cloneNode(true);
  cell.querySelector('.cell-number').textContent = `${i + 1}`;
  list.append(cell);
}
*,
*::after,
*::before {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;

    height: 100vh;

    background-color: #222;
}

.list {
    display: grid;
    grid-template-columns: repeat(7, auto);
    gap: 3px;
}

.cell {
    display: flex;
    justify-content: center;
    align-items: center;

    width: 50px;
    aspect-ratio: 1.0;

    border: 2px solid transparent;

    color: #eaeaea;

    list-style: none;
    user-select: none;
}

.cell:hover {
    border-color: #777;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <link rel="stylesheet" href="style.css">
</head>

<body>
    <ul class="list"></ul>

    <template id="cell-template">
        <li class="cell">
            <span class="cell-number"></span>
        </li>
    </template>

    <script src="script.js"></script>
</body>

</html>


Ответы (1 шт):

Автор решения: Vladislav G.

В общем немного заморочился и придумал такое решение:

const cellTemplate = document.querySelector('#cell-template').content.querySelector('.cell');
const subcellTemplate = document.querySelector('#subcell-template').content.querySelector('.subcell');

const container = document.querySelector('.container');
const sublist = document.querySelector('.list-subcells');
const list = document.querySelector('.list-cells');
const shadow = document.querySelector('.shadow');

function calcMouseCrd ({ x, y }) {
    return {
        x: x - container.offsetLeft - 0.5 * shadow.offsetWidth,
        y: y - container.offsetTop - 0.5 * shadow.offsetHeight
    };
}

function setShadowCrd ({ x, y }) {
    shadow.style.left = `${x}px`;
    shadow.style.top = `${y}px`;
}

(function () {
    for (let i = 0; i < 30; i++) {
        const cell = cellTemplate.cloneNode(true);
        const subcell = subcellTemplate.cloneNode(true);
        cell.querySelector('.cell-number').textContent = `${i + 1}`;
        list.append(cell);
        sublist.append(subcell);
    }

    setShadowCrd(calcMouseCrd({x: 0, y: 0}));

    document.addEventListener('mousemove', (evt) => {
        const { x, y } = calcMouseCrd({ x: evt.clientX, y: evt.clientY });
    
        setShadowCrd({ x, y });
    });
})();
*,
*::after,
*::before {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;

    height: 100vh;

    background-color: #222;
}

.container {
    position: relative;
    display: grid;

    overflow: hidden;
}

.list {
    grid-row: 1/2;
    grid-column: 1/2;

    display: grid;
    grid-template-columns: repeat(7, auto);
    gap: 3px;

    z-index: 1;
}

.list-subcells {
    z-index: 0;
}

.shadow {
    position: absolute;
    width: 200vw;
    aspect-ratio: 1.0;

    background: radial-gradient(rgba(34, 34, 34, 0), #222 100px);
}

.cell {
    display: flex;
    justify-content: center;
    align-items: center;

    width: 50px;
    aspect-ratio: 1.0;

    border: 2px solid transparent;

    color: #eaeaea;

    list-style: none;
    user-select: none;
}

.cell:hover {
    border-color: #999;
}

.subcell {
    border-color: #666;
    pointer-events: none;
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="container">
        <ul class="list list-subcells"></ul>
        <div class="shadow"></div>
        <ul class="list list-cells"></ul>
    </div>

    <template id="cell-template">
        <li class="cell">
            <span class="cell-number"></span>
        </li>
    </template>

    <template id="subcell-template">
        <li class="cell subcell"></li>
    </template>

    <script src="script.js"></script>
</body>

</html>

Решение основывается на том, что под список с ячейками календаря добавляется такой же список с пустыми ячейками subcell, для которых задан постоянный цвет границы, отличающийся от фона. Оба списка помещаются в grid-контейнер и накладываются друг на друга, а между ними помещается пустой div shadow с радиальным градиентом, цвет в градиенте не меняется, меняется только прозрачность - она становится максимальной в центре. Далее этот shadow "привязывается" своим центром к курсору. Может кому то пригодится.

А вообще было бы интересно посмотреть и другие подходы к реализации. Я ничего другого придумать не смог.

→ Ссылка