如何在 CSS 中映射鼠标位置

Avatar of Amit Sheen
Amit Sheen

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费赠送积分!

让我们看看如何获取用户的鼠标位置并将其映射到 CSS 自定义属性:--positionX--positionY

我们可以 在 JavaScript 中执行此操作。 如果我们这样做了,我们可以做一些事情,比如使 元素可拖动 或者 移动背景。 但实际上,我们仍然可以做类似的事情,但不用任何 JavaScript!

我将此方法用作我为获得 ‘单击并拖动’ 纯 CSS 效果制作的演示的一部分。 我使用了 我之前文章 中的 perspective 提示。 这是一个非常不错的效果,可以在 CSS 中完全实现,它可能比我的演示有更广泛的用途,所以让我们看一下。

设置

我们的第一个演示将使用 --positionX--positionY 自定义属性来设置元素的 widthheight

提醒您,我们在这里只使用 SCSS 来简短起见,但所有这些都可以在纯 CSS 中完成。

这是我们的初始状态。 我们这里有一个带有 .content 类的‘包装器’ <div>,该类扩展到 body 的宽度和高度。 此 <div> 将承载我们项目的内容,以及我们想要使用鼠标位置控制的元素 - 在这种情况下,是 .square 元素。

我们还将两个自定义属性添加到 content 中。 我们将使用鼠标位置来设置这些属性的值,然后使用它们来相应地设置 .square 元素的 widthheight

一旦我们映射了鼠标位置的自定义属性,我们就可以使用它们来执行我们想要的任何操作。 例如,我们可以使用它们来设置 absolute 定位的元素的 top / left 属性,控制 transform 属性,设置 background-position,操作颜色,甚至设置伪元素的内容。 我们将在文章末尾看到一些这些额外演示。

网格

目标是在屏幕上创建一个不可见的网格,并使用 :hover 伪类将每个‘单元格’映射到我们将分配给自定义属性的一组值。 因此,当鼠标光标移到屏幕右侧时,--positionX 的值会更高; 当它移到左侧时,它会更低。 我们将对 --positionY 做同样的事情:当光标移到顶部时,该值会更低,当它移到底部时,该值会更高。

关于我们正在使用的网格大小,我们要说几句话:我们实际上可以将网格的大小设置为我们想要的任何大小。 网格越大,我们的自定义属性值就越精确。 但这也意味着我们将有更多单元格,这会导致性能问题。 重要的是要保持适当的平衡,并将网格大小调整到每个项目的特定需求。

现在,假设我们想要一个 10×10 的网格,在我们的标记中总共有 100 个单元格。(是的,使用 Pug 来实现这一点是可以的,即使我在本示例中不会这样做。)

<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<!-- 97 more cells -->

<div class="content">
  <div class="square"></div>
</div>

您可能想知道为什么 .cell 元素出现在 .content 之前。 那是因为级联。

我们想要使用 .cell 类来控制 .square,级联的工作方式(目前)是元素只能控制其子元素(或后代)及其兄弟姐妹(或其后代) - 但只有当兄弟姐妹位于控制元素之后时才有效。

这意味着两件事

  1. 每个 .cell 必须位于我们想要控制的元素(在本例中为 .square)之前。
  2. 我们不能将这些 .cell 放入容器中,因为如果我们这样做,.content 就不是它们的兄弟姐妹了。

定位单元格

有几种方法可以定位 .cell。 我们可以使用 position: absolute 将它们定位,并使用 topleft 属性偏移它们。 或者我们可以使用 transform 将它们平移到位置。 但最简单的方法可能是使用 display: grid

body {
  background-color: #000;
  height: 100vh;
  display: grid;
  grid-template: repeat(10, 1fr) / repeat(10, 1fr);
}

.cell {
  width: 100%;
  height: 100%;
  border: 1px solid gray;
  z-index: 2;
}
  • border 只是暂时的,所以我们可以看到屏幕上的 cell。 我们稍后会将其删除。
  • z-index 很重要,因为我们希望 cell 位于 content 的前面。

这是我们到目前为止所拥有的内容

添加值

我们想要使用 .cell 来设置 --positionX--positionY 值。

当我们悬停在第一列(左侧)的 .cell 上时,--positionX 的值应为 0。 当我们悬停在第二列的 .cell 上时,该值应为 1。 在第三列中应为 2,依此类推。

y 轴也是如此。 当我们悬停在第一行(顶部)的 .cell 上时,--positionY 应为 0,当我们悬停在第二行的 cell 上时,该值应为 1,依此类推。

A black ten by ten grid of squares with white borders and numbers in sequential order from left to right.

此图像中的数字表示网格中单元格元素的编号。 如果我们以单个 .cell 为例 - 假设是第 42 个单元格 - 我们可以使用 :nth-child() 来选择它

.cell:nth-child(42) { }

但我们需要记住几件事

  1. 我们只希望此选择器在我们将鼠标悬停在单元格上时有效,因此我们将 :hover 附加到它。
  2. 我们想要选择 .content 而不是单元格本身,因此我们将使用 通用兄弟选择器 (~) 来执行此操作。

因此,现在,要将 --positionX 设置为 1,将 --positionY 设置为 3,以便在将鼠标悬停在第 42 个 cell 上时为 .content 设置这些值,我们需要执行以下操作

.cell:nth-child(42):hover ~ .content {
  --positionX: 1;
  --positionY: 3;
}

但是谁想做 100 次呢!? 有几种方法可以简化操作

  1. 使用 Sass @for 循环 遍历所有 100 个单元格,并进行一些计算以在每次迭代中设置正确的 --positionX--positionY 值。
  2. 分别使用 x 轴和 y 轴,分别使用 :nth-child函数式表示法 来单独选择每一行和每一列。
  3. 将这两种方法结合起来,使用 Sass @for 循环以及 :nth-child 的函数式表示法。

我认真思考了哪种方法是最好的、最简单的方法,虽然它们都有优缺点,但我最终选择了第三种方法。 要编写的代码量、编译代码的质量以及数学复杂度都影响了我的思考。 不同意? 在评论中告诉我原因!

使用 @for 循环设置值

@for $i from 0 to 10 {
 .cell:nth-child(???):hover ~ .content {
    --positionX: #{$i};
  }
  .cell:nth-child(???):hover ~ .content {
    --positionY: #{$i};
  }
}

这是基本循环。 我们要循环 10 次,因为我们有 10 行和 10 列。 我们已经将 x 轴和 y 轴分开,分别为每一列设置 --positionX,为每一行设置 --positionY。 我们现在只需要用适当的表示法替换那些 ??? 东西来选择每一行和每一列。

让我们从 x 轴开始

回到我们的网格图像(带有数字的那个),我们可以看到第二列中所有单元格的编号都是 10 的倍数,再加上 2。 第三列中的单元格是 10 的倍数,再加上 3。 依此类推。

现在让我们将其‘翻译’成 :nth-child 的函数式表示法。 以下是第二列的表示方式

:nth-child(10n + 2)
  • 10n 选择 10 的所有倍数。
  • 2 是列号。

对于我们的循环,我们将用 #{$i + 1} 替换列号,以便按顺序进行迭代

.cell:nth-child(10n + #{$i + 1}):hover ~ .content {
  --positionX: #{$i};
}

现在我们来处理y轴

再次查看网格图像,并关注第四行。单元格编号介于 41 和 50 之间。第五行的单元格介于 51 到 60 之间,依此类推。要选择每一行,我们需要定义它的范围。例如,第四行的范围是

.cell:nth-child(n + 41):nth-child(-n + 50)
  • (n + 41) 是范围的起始位置。
  • (-n + 50) 是范围的结束位置。

现在我们将用$i值的一些数学运算替换数字。对于范围的开始,我们得到(n + #{10 * $i + 1}),对于范围的结束,我们得到(-n + #{10 * ($i + 1)})

所以最终的@for循环是

@for $i from 0 to 10 {
 .cell:nth-child(10n + #{$i + 1}):hover ~ .content {
    --positionX: #{$i};
  }
  .cell:nth-child(n + #{10 * $i + 1}):nth-child(-n + #{10 * ($i + 1)}):hover ~ .content {
    --positionY: #{$i};
  }
}

映射完成!当我们悬停在元素上时,--positionX--positionY根据鼠标位置改变。这意味着我们可以用它们来控制.content内部的元素。

处理自定义属性

好的,现在我们已经将鼠标位置映射到两个自定义属性,接下来我们要用它们来控制.square元素的widthheight值。

我们先从width开始,假设我们想要.square的最小width100px(即当鼠标光标位于屏幕左侧时),并且我们希望它每当鼠标光标向右移动一步就增长20px

使用calc(),我们可以做到

.square {
  width: calc(100px + var(--positionX) * 20px);
}

当然,我们也会对height做同样的事情,但使用--positionY而不是--positionX

.square {
  width: calc(100px + var(--positionX) * 20px);
  height: calc(100px + var(--positionY) * 20px);
}

就这样!现在我们有一个简单的.square元素,它的widthheight由鼠标位置控制。将鼠标光标移动到窗口上,观察square如何根据鼠标位置改变它的widthheight

我添加了一个小的transition来使效果更平滑。当然,这不是必需的。我还在.cell边框上添加了注释。

让我们尝试另一种方法

可能会有这样一种情况,你想要“绕过”--positionX--positionY,并在@for循环中直接设置结束值。所以,对于我们的示例,它看起来像这样

@for $i from 0 to 10 {
 .cell:nth-child(10n + #{$i + 1}):hover ~ .content {
    --squareWidth: #{100 + $i * 20}px;
  }
  .cell:nth-child(n + #{10 * $i + 1}):nth-child(-n + #{10 * ($i + 1)}):hover ~ .content {
    --squareHeight: #{100 + $i * 20}px;: #{$i};
  }
}

那么.square将像这样使用自定义属性

.square {
  width: var(--squareWidth);
  height: var(--squareHeight);
}

这种方法稍微灵活一些,因为它允许使用更高级的Sass数学(和字符串)函数。也就是说,基本原理与我们已经介绍的完全相同。

下一步是什么?

好吧,剩下的就由你决定了——而且可能性是无限的!你认为你会如何使用它?你能进一步扩展它吗?尝试在你的CSS中使用这个技巧,并在评论中分享你的作品,或者在Twitter上告诉我。看到这些作品汇集在一起会很棒。

这里有一些例子可以让你开始思考