首頁 > 軟體

JS實現一個可以當鏡子照的 Button

2023-03-07 06:02:19

正文

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

它看起來就像我上廁所時看到的這個東西一樣


IT145.com E-mail:sddin#qq.com