目录
- 正文
- mediaDevices 的介绍
- 我们这里用到getUserMedia 的 api
- 把获取到的 stream 用一个 video 来展示
- 样式部分
- 阴影的设置
- 完整代码
- 总结
正文
最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。
那这个好玩的 Button 是怎么实现的呢?
很容易想到是用到了摄像头。
没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。
button 的部分倒是很容易,主要是阴影稍微麻烦点。
把 video 作为 button 的子元素,加个 overflow:hidden 就完成了上面的效果。
思路很容易,那我们就来实现下吧。
获取摄像头用的是 navigator.mediaDevices.getUserMedia 的 api。
mediaDevices 的介绍
在 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。
它看起来就像我上厕所时看到的这个东西一样😂: