服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

写一个可以当镜子照的 Button

日期: 来源:神光的编程秘籍收集编辑:神说要有光zxg

最近写了一个好玩的 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。

当然,用在这里的话还需要设置下 filter 以及 object-fit、object-position 等样式。

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

相关阅读

  • 可视化搭建 - 定义联动协议

  • 虽然底层框架提供了通用的组件值与联动配置,可以建立对组件任意 props 的映射,但这只是一个能力,还不是协议。业务层是可以确定一个协议的,还要让这个协议具有拓展性。我们先从
  • 住院15天必须出院?官方回应→

  • 近日,国家医保局发布对十三届全国人大五次会议第6976号建议的答复。就部分医院为了提高病床周转率而规定患者15天必须出院等问题,国家医保局和各级医保部门对参保患者住院天数
  • 全部辞退!

  • 近日自嗨锅因“176万致富款花蛤粉”被指侮辱死者陷入舆论风波公司回应相关涉事人员已被辞退3月4日,自嗨锅官方微博发布致歉声明称,“176万宣传图”事件系公司旗下一家直营店运
  • 最近几年,人工智能和深度学习迅猛发展,在语言、艺术、医疗、金融等多种领域展现出了强大的能力。其中艺术领域发展出来的AI绘画相信大家都已经不陌生了。虽然AI绘画不需要自己

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 文物说节气|惊蛰

  • 国家文物局新闻中心新华社新媒体中心联合出品编校:赵军慧审核:徐秀丽监制:李 让
  • 今天,两家央媒关注贵州

  • 3月6日,《人民日报》整版刊发《树牢开放理念、擦亮开放名片、高扬开放旗帜——贵州以高水平开放促进高质量发展》,聚焦近年来,贵州坚持把对外开放作为后发赶超的战略支撑,加快提
  • 可视化搭建 - 定义联动协议

  • 虽然底层框架提供了通用的组件值与联动配置,可以建立对组件任意 props 的映射,但这只是一个能力,还不是协议。业务层是可以确定一个协议的,还要让这个协议具有拓展性。我们先从
  • 写一个可以当镜子照的 Button

  • 最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。那这个好玩的 Button 是怎么实现的呢?很容易想到是用到了摄像头。没错,这里要使用浏览器的获取媒体设备的