时间:2023-03-09来源:系统城装机大师作者:佚名
最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。
那这个好玩的 Button 是怎么实现的呢?
很容易想到是用到了摄像头。
没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。
button 的部分倒是很容易,主要是阴影稍微麻烦点。
把 video 作为 button 的子元素,加个 overflow:hidden 就完成了上面的效果。
思路很容易,那我们就来实现下吧。
获取摄像头用的是 navigator.mediaDevices.getUserMedia 的 api。
在 MDN 中可以看到 mediaDevices 的介绍:
可以用来获取摄像头、麦克风、屏幕等。
它有这些 api:
getDisplayMedia 可以用来录制屏幕,截图。
getUserMedia 可以获取摄像头、麦克风的输入。
它要指定音频和视频的参数,开启、关闭、分辨率、前后摄像头啥的:
这里我们把 video 开启,把 audio 关闭。
也就是这样:
1 2 3 4 5 6 7 8 9 |
navigator.mediaDevices.getUserMedia({ video: true , audio: false , }) .then((stream) => { //... }). catch (e => { console.log(e) }) |
1 2 3 4 5 6 7 8 9 10 11 12 |
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:
1 2 3 |
video { filter: blur( 10px ); } |
加点饱和度:
1 2 3 |
video { filter: saturate( 5 ) } |
或者加点亮度:
1 2 3 |
video: { filter: brightness( 3 ); } |
filter 可以组合,调整调整达到这样的效果就可以了:
1 2 3 |
video { filter: blur( 2px ) saturate( 0.6 ) brightness( 1.1 ); } |
然后调整下大小:
1 2 3 4 5 |
video { width : 300px ; height : 100px ; filter: blur( 2px ) saturate( 0.6 ) brightness( 1.1 ); } |
你会发现视频的画面没有达到设置的宽高。
这时候通过 object-fit 的样式来设置:
1 2 3 4 5 6 |
video { width : 300px ; height : 100px ; object-fit: cover; filter: blur( 2px ) saturate( 0.6 ) brightness( 1.1 ); } |
cover 是充满容器,也就是这样:
但画面显示的位置不大对,看不到脸。我想显示往下一点的画面怎么办呢?
可以通过 object-position 来设置:
1 2 3 4 5 6 7 |
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 周反转了。
1 2 3 4 5 6 7 8 |
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 部分,这个我们倒是经常写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
:root { --transition: 0.1 s; --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 黑色阴影:
1 2 3 |
.button.pressed { box-shadow: 0px -1px 1px rgba( 0 , 0 , 0 , 0.5 ), 0px 1px 1px rgba( 0 , 0 , 0 , 0.5 ); } |
同时,按下时还有个 scale 的设置:
再就是文字的阴影,也是上下都设置了 1px 阴影,达到环绕的效果:
1 | text-shadow : 0px -1px 0px rgba( 255 , 255 , 255 , 0.5 ), 0px 1px 0px rgba( 255 , 255 , 255 , 0.5 ); |
最后,把这个 video 嵌进去就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
body { padding : 200px ; } :root { --transition: 0.1 s; --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。
它看起来就像我上厕所时看到的这个东西一样
2023-03-18
如何使用正则表达式保留部分内容的替换功能2023-03-18
gulp-font-spider实现中文字体包压缩实践2023-03-18
ChatGPT在前端领域的初步探索最近闲来无事,在自己的小程序里面集成了一个小视频的接口,但是由于小程序对于播放视频的限制,只能用来做一个demo刷视频了,没办法上线体验。小程序播放视频限制最多10个,超出可能...
2023-03-18
Vue.js、React和Angular对比 以下是Vue.js的代码示例: 以下是React的代码示例: 以下是Angular的代码示例:...
2023-03-18