注册
web

写一个可以当镜子照的 Button

最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。



那这个好玩的 Button 是怎么实现的呢?


很容易想到是用到了摄像头。


没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。


button 的部分倒是很容易,主要是阴影稍微麻烦点。


把 video 作为 button 的子元素,加个 overflow:hidden 就完成了上面的效果。


思路很容易,那我们就来实现下吧。


获取摄像头用的是 navigator.mediaDevices.getUserMedia 的 api。


在 MDN 中可以看到 mediaDevices 的介绍:



可以用来获取摄像头、麦克风、屏幕等。


它有这些 api:



getDisplayMedia 可以用来录制屏幕,截图。


getUserMedia 可以获取摄像头、麦克风的输入。



我们这里用到的是 getUserMedia 的 api。


它要指定音频和视频的参数,开启、关闭、分辨率、前后摄像头啥的:



这里我们把 video 开启,把 audio 关闭。


也就是这样:


navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
//...
}).catch(e => {
console.log(e)
})

然后把获取到的 stream 用一个 video 来展示:


navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
const video = document.getElementById('video');
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
})
.catch((e) => console.log(e));

就是这样的:



通过 css 的 filter 来加点感觉:


比如加点 blur:


video {
filter: blur(10px);
}


加点饱和度:


video {
filter: saturate(5)
}



或者加点亮度:


video: {
filter: brightness(3);
}


filter 可以组合,调整调整达到这样的效果就可以了:


video {
filter: blur(2px) saturate(0.6) brightness(1.1);
}


然后调整下大小:


video {
width: 300px;
height: 100px;
filter: blur(2px) saturate(0.6) brightness(1.1);
}


你会发现视频的画面没有达到设置的宽高。


这时候通过 object-fit 的样式来设置:


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
}

cover 是充满容器,也就是这样:



但画面显示的位置不大对,看不到脸。我想显示往下一点的画面怎么办呢?


可以通过 object-position 来设置:


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
}

y 向下移动 100 px ,也就是这样的:



现在画面显示的位置就对了。


其实现在还有一个特别隐蔽的问题,不知道大家发现没,就是方向是错的。照镜子的时候应该左右翻转才对。


所以加一个 scaleX(-1),这样就可以绕 x 周反转了。


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
transform: scaleX(-1);
}


这样就是镜面反射的感觉了。


然后再就是 button 部分,这个我们倒是经常写:


function Button({ children }) {
const [buttonPressed, setButtonPressed] = useState(false);

return (
<div
className={`button-wrap ${buttonPressed ? "pressed" : null}`}
>

<div
className={`button ${buttonPressed ? "pressed" : null}`}
onPointerDown={() =>
setButtonPressed(true)}
onPointerUp={() => setButtonPressed(false)}
>
<video/>
</div>
<div className="text">{children}</div>
</div>

);
}

这里我用 jsx 写的,点击的时候修改 pressed 状态,设置不同的 class。


样式部分是这样的:


:root {
--transition: 0.1s;
--border-radius: 56px;
}

.button-wrap {
width: 300px;
height: 100px;
position: relative;
transition: transform var(--transition), box-shadow var(--transition);
}

.button-wrap.pressed {
transform: translateZ(0) scale(0.95);
}

.button {
width: 100%;
height: 100%;
border: 1px solid #fff;
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15),
0px 16px 32px rgba(0, 0, 0, 0.125);
transform: translateZ(0);
cursor: pointer;
}

.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

.text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: rgba(0, 0, 0, 0.7);
font-size: 48px;
font-weight: 500;
text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);
}

这种 button 大家写的很多了,也就不用过多解释。


要注意的是 text 和 video 都是绝对定位来做的居中。


再就是阴影的设置。


阴影的 4 个值是 x、y、扩散半径、颜色。


我设置了个多重阴影:




然后再改成不同透明度的黑就可以了:



再就是按下时的阴影,设置了上下位置的 1px 黑色阴影:


.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

同时,按下时还有个 scale 的设置:



再就是文字的阴影,也是上下都设置了 1px 阴影,达到环绕的效果:


text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);


最后,把这个 video 嵌进去就行了。


完整代码如下:


import React, { useState, useEffect, useRef } from "react";
import "./button.css";

function Button({ children }) {
const reflectionRef = useRef(null);
const [buttonPressed, setButtonPressed] = useState(false);

useEffect(() => {
if (!reflectionRef.current) return;
navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
const video = reflectionRef.current;
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
})
.catch((e) => console.log(e));
}, [reflectionRef]);

return (
<div
className={`button-wrap ${buttonPressed ? "pressed" : null}`}
>

<div
className={`button ${buttonPressed ? "pressed" : null}`}
onPointerDown={() =>
setButtonPressed(true)}
onPointerUp={() => setButtonPressed(false)}
>
<video
className="button-reflection"
ref={reflectionRef}
/>

</div>
<div className="text">{children}</div>
</div>

);
}

export default Button;

body {
padding: 200px;
}
:root {
--transition: 0.1s;
--border-radius: 56px;
}

.button-wrap {
width: 300px;
height: 100px;
position: relative;
transition: transform var(--transition), box-shadow var(--transition);
}

.button-wrap.pressed {
transform: translateZ(0) scale(0.95);
}

.button {
width: 100%;
height: 100%;
border: 1px solid #fff;
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15),
0px 16px 32px rgba(0, 0, 0, 0.125);
transform: translateZ(0);
cursor: pointer;
}

.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

.text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: rgba(0, 0, 0, 0.7);
font-size: 48px;
font-weight: 500;
text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);
}

.text::selection {
background-color: transparent;
}

.button .button-reflection {
width: 100%;
height: 100%;
transform: scaleX(-1);
object-fit: cover;
opacity: 0.7;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
}

总结


浏览器提供了 media devices 的 api,可以获取摄像头、屏幕、麦克风等的输入。


除了常规的用途外,还可以用来做一些好玩的事情,比如今天这个的可以照镜子的 button。


它看起来就像我上厕所时看到的这个东西一样😂:



作者:zxg_神说要有光
来源:juejin.cn/post/7206249542752567333

0 个评论

要回复文章请先登录注册