30daysJS-16 - Mouse Shadow
这个就更是纯数学了。
问题
核心坑点:
- 事件冒泡;
- offset是上级和下级之间的距离关系;
- 偏移量函数;
- text-shadow可以设定多个值。
核心思路:
我鼠标无论移动到哪,实际上改变的都是映射到偏移量正方形里点的坐标系。
问题一:事件冒泡+offset
hero.addEventListener('mousemove', shadow);
let { offsetX: x, offsetY: y } = e;
if (this !== e.target) {
x = x + e.target.offsetLeft;
y = y + e.target.offsetTop;
}
this始终指向是事件绑定(也就是发生时)的Node节点对象,而e.target指向的是引发事件的那个嵌套层级最深的元素,二者不一定相等,导致二者的上级元素也不一定相等。
offset相关指的是上级和下级之间的距离关系,这可能就会出现x,y返回的是e.target和某一元素内部的距离,我们想要的是目标点和视口的距离关系,所以需要保证无论e.target在哪,返回的恒定为视口和目标点的距离关系。
x,y在判定之前已被赋值,是鼠标和事件触发元素内部的距离关系,之后判定内赋值,是加上了事件触发元素和上级元素之间的距离关系。
注意:
offsetX, offsetY 和offsetLeft, offsetTop是不一样的:
offsetX, offsetY所属对象是MouseEvent事件对象;offsetLeft, offsetTop所属对象是HTMLElementDOM元素对象。
另外:
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其parent上的处理程序,然后一直向上到其他ancestors上的处理程序。
引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。
问题二:偏移量
仔细观察我们要实现的效果图,便可以看出它们全是围绕着一个中心点旋转。
const xWalk = Math.round((x / width) * walk) - (walk / 2));
这两个变量:
(x / width) * walk:
- x: 鼠标和视口最左边的距离;
- width:元素宽度;
- walk: 设定好的偏移量,一个系数;
- x / width:将x转换为比例值(0到1),计算鼠标在元素中的相对位置
- (x / width) * walk:再乘以walk便是将x映射到
[0, walk]范围,得到始终是一个正值。
(x / width) * walk - walk / 2
- 这个是人为地制造了一个
[- walk / 2 , walk / 2 ]的轴,使(x / width) * walk - walk / 2 的值始终在[- walk / 2 , walk / 2 ]中偏移。 - 线性变化:将区间
[0, width]映射到区间[-walk/2, walk/2]; - 属于数据标准化的技巧。
- 这个是人为地制造了一个
想象一个尺子:
原始: 0──────25♠──────50──────75──────100
鼠标在这里
减去50:-50──────-25♠──────0──────25──────50
鼠标变成-25
如果walk = 100
x (鼠标位置) | normalized = x/width*100 | xWalk = normalized - 50
------------|-------------------------|-------------------------
0 | 0 | -50
width/4 | 25 | -25
width/2 | 50 | 0
3*width/4 | 75 | 25
width | 100 | 50
最终我们得到一个[- walk / 2 , walk / 2 ]间的x值,同理,y值也是这样计算出,(x, y)坐标便是在一个正方形区域内移动了:

问题三:多值语法
text-shadow 属性支持用逗号分隔的多个阴影值,从第一个开始渲染:
text-shadow: h-shadow1 v-shadow1 blur-radius1 color1,
h-shadow2 v-shadow2 blur-radius2 color2,
...;
其他支持多值的CSS属性:
box-shadow;background;filter;transform.
!注意!
多值语法的最后一项末尾不能加,逗号,会造成语法错误!!!(血泪教训T T
问题四: 解构赋值
const {offsetWidth: width, offsetHeight: height} = hero
let { offsetX: x, offsetY: y } = e
等于
const width = hero.offsetWidth
const width = hero.offsetHeight
let x = e.offsetX
let y = e.offsetY
答案代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mouse Shadow</title>
<link rel="icon" href="https://fav.farm/✅" />
</head>
<body>
<div class="hero">
<h1 contenteditable>🔥WOAH!</h1>
</div>
<style>
html {
color: black;
font-family: sans-serif;
}
body {
margin: 0;
}
.hero {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: black;
}
h1 {
font-size: 100px;
}
</style>
<script>
const hero = document.querySelector('.hero');
const text = hero.querySelector('h1');
const walk = 100;
function shadow(e) {
const { offsetWidth: width, offsetHeight: height } = hero;
let { offsetX: x, offsetY: y } = e;
console.log(e, e.target);
if (this !== e.target) {
x = x + e.target.offsetLeft;
y = y + e.target.offsetTop;
}
const xWalk = Math.round((x / width * walk) - (walk / 2));
const yWalk = Math.round((y / height * walk) - (walk / 2));
text.style.textShadow = `
${xWalk}px ${yWalk}px 0 rgba(255,0,255,0.7),
${xWalk * -1}px ${yWalk}px 0 rgba(0,255,255,0.7),
${yWalk}px ${xWalk * -1}px 0 rgba(0,255,0,0.7),
${yWalk * -1}px ${xWalk}px 0 rgba(0,0,255,0.7)
`;
}
hero.addEventListener('mousemove', shadow);
</script>
</body>
</html>