IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Use Gyro Data to Make Your React App More Interactive

    Lynan发表于 2023-12-11 06:26:08
    love 0

    Cover Image Credit: https://unsplash.com/photos/uCPHkM2bnaI

    https://trekhleb.dev/blog/2021/gyro-web/
    In Javascript you may access your device orientation data by listening to the deviceorientation event. It is as easy as the following:

    1
    2
    3
    4
    5
    6
    7
    8
    window.addEventListener("deviceorientation", handleOrientation);

    function handleOrientation(event) {
    const alpha = event.alpha;
    const beta = event.beta;
    const gamma = event.gamma;
    // Do stuff...
    }

    Description Of Deviceorientation Data


    This Demo Can Be Visited In Here

    The demo video was compressed by https://clideo.com/ from 4.3MB to 93KB, that’s impressive.

    To achieve the effect above, we need to disassemble it into 2 steps:

    1. Show the hidden content of the specified area
    2. Make the “lens” element move with device gyro sensor data

    Step 1. Show the hidden content of the specified area

    How did this magic happen?

    If you were familiar with Photoshop you would know there’s a magic trick property mask, we have that in CSS too. Mask property reference document-MDN

    Check caniuse for compatibility and the result is in below. We can see that we have about 97% browser support. For mobile Android device, we need to add the -webkit prefix before the mask attribute to make it effective.

    Can I Use "Mask" In CSS

    Then, let’s test it out.

    First, we add a block with backgroud color

    index.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <style>
    .example {
    width: 200px;
    height: 200px;
    background-color: red;
    text-align: center;
    box-sizing: border-box;
    padding-top: 30px;
    }
    </style>

    <div class="example">a quick brown fox jumps over the lazy dog</div>
    a quick brown fox jumps over the lazy dog

    Then, we add a rounded mask on it

    Before adding the mask image, we should know how did the mask work with our background layer.
    Among the multiple mask properties of CSS, the mask-image property sets the mask layer image, and its property value is very similar to background-image, which can be <url> or <gradient>.

    When we set mask-image to <url> or <gradient>, the value of mask-mode attribute is: alpha. This means that the background element and the mask layer element overlap, and the background layer will show through from the non-transparent part of the mask layer.

    1
    2
    3
    -webkit-mask-image: url("https://article-assets.lynan.cn/rounded-mask-59bf39.png");
    -webkit-mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    a quick brown fox jumps over the lazy dog

    Through practice, we can see that no matter what color the non-transparent areas of the mask is, it will not make any different mask effect.

    On iOS devices, when obtaining gyroscope data authorization for the first time, there will be a system pop-up window asking whether to allow access to gyroscope data, while Android devices are silently authorized without pop-up windows.

    Step2. Make The Lens Element Move With Gryo Sensor Data

    Follow the guidance in Mobile Device Orientation – New Now we will have the useDeviceOrientation hook.

    But How Did the Hidden Text Move?

    By using the mask-position property, we can move the position of the mask layer.

    It should be noted that when running on a real device, when the iOS device renders the transition of mask-position, it will feel a half-beat slower than the transition of transform of the lens element, because mask-position ` Rendering requires a relatively large overhead. *Tested models are: iPhone 14 Pro (iOS 16.4.0) and Samsung S21+ (Android 13)

    “mask-position” | Can I use… Support tables for HTML5, CSS3, etc
    In this case, to make the Magnifying lens element follow our gyroscope data, we need two layers: one to show the magnifying glass and make it move, and one to hold the hidden information beneath the magnifying glass.

    The magnifying lens element follows the movement and uses transform: translate() to update the position. When the magnifying lens moves, the mask-position of the hidden information layer below is updated at the same time, so that the mask position of the layer is also updated together, and you can get Hidden elements are always displayed under the magnifying lens element.

    I made a CodePen for this demo

    Compiled HTML is in below.

    index.html
    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Gryro With React</title>
    <style>
    body {
    height: 100vh;
    margin: 0;
    display: grid;
    place-items: center;
    }

    .box {
    width: auto;
    }

    .bg {
    margin: 0 auto;
    width: 85.06666667vw;
    height: 55vw;
    background-image: url("https://i.imgur.com/FZNcwts.png");
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    position: relative;
    }
    .bg .hiddenText {
    font-size: 3.7vw;
    transform: translateZ(0);
    position: absolute;
    box-sizing: border-box;
    padding-top: 12.26666667vw;
    left: 0;
    width: 100%;
    height: 100%;
    text-align: center;
    -webkit-mask-size: 34.13333333vw 34.13333333vw;
    mask-size: 34.13333333vw 34.13333333vw;
    -webkit-mask-image: url("https://i.imgur.com/nWRUuqv.png");
    mask-image: url("https://i.imgur.com/nWRUuqv.png");
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    transition: all ease 0.2s;
    -webkit-mask-position: 25.565617vw 6.13333333vw;
    mask-position: 25.565617vw 6.13333333vw;
    }
    .bg .lens {
    position: absolute;
    top: 6.13333333vw;
    left: 25.33333333vw;
    background-image: url("https://i.imgur.com/FOUMIQ6.png");
    background-size: 100% 100%;
    background-repeat: no-repeat;
    width: 34.13333333vw;
    height: 34.13333333vw;
    transition: transform ease 0.2s;
    transform: translateX(74.9%);
    }
    </style>
    </head>
    <body>
    <div id="root"></div>
    <script type="module">
    import React, {
    useCallback,
    useEffect,
    useState,
    } from "https://esm.sh/react@18";
    import ReactDOM from "https://esm.sh/react-dom@18";
    import throttle from "https://cdn.skypack.dev/lodash@4.17.21/throttle";
    const useDeviceOrientation = () => {
    const [error, setError] = useState(null);
    const [orientation, setOrientation] = useState(null);
    const onDeviceOrientation = throttle((event) => {
    setOrientation({
    alpha: event.alpha,
    beta: event.beta,
    gamma: event.gamma,
    });
    }, 100);
    const revokeAccessAsync = async () => {
    window.removeEventListener("deviceorientation", onDeviceOrientation);
    setOrientation(null);
    };
    const requestAccessAsync = async () => {
    if (!DeviceOrientationEvent) {
    setError(
    new Error(
    "Device orientation event is not supported by your browser"
    )
    );
    return false;
    }
    if (
    DeviceOrientationEvent.requestPermission &&
    typeof DeviceMotionEvent.requestPermission === "function"
    ) {
    let permission;
    try {
    permission = await DeviceOrientationEvent.requestPermission();
    } catch (err) {
    setError(err);
    return false;
    }
    if (permission !== "granted") {
    setError(
    new Error(
    "Request to access the device orientation was rejected"
    )
    );
    return false;
    }
    }
    window.addEventListener("deviceorientation", onDeviceOrientation);
    return true;
    };
    const requestAccess = useCallback(requestAccessAsync, []);
    const revokeAccess = useCallback(revokeAccessAsync, []);
    useEffect(() => {
    return () => {
    revokeAccess();
    };
    }, [revokeAccess]);
    return {
    orientation,
    error,
    requestAccess,
    revokeAccess,
    };
    };
    const getTransformDegree = (value, threshold = 30, max = 74.9) => {
    if (value) {
    const sy = value > 0 ? "" : "-";
    const degree = `${Math.min(
    (Math.abs(value) / threshold) * 100,
    max
    )}`;
    return Number(`${sy}${degree}`);
    }
    return 0;
    };
    const Demo = ({ orientation }) => {
    const degree = getTransformDegree(
    orientation === null || orientation === void 0
    ? void 0
    : orientation.gamma
    );
    const yDegree = getTransformDegree(
    orientation === null || orientation === void 0
    ? void 0
    : orientation.beta,
    20,
    40
    );
    const transform = `translate(${degree}%, ${yDegree}%)`;
    const maskPosition = `${25.565617 + (degree / 100) * 34.133}vw calc(${
    25.565617 + (yDegree / 100) * 34.133
    }vw - 18.4vw)`;
    return React.createElement(
    "div",
    { className: "bg" },
    React.createElement(
    "div",
    {
    className: "hiddenText",
    style: {
    maskPosition,
    "-webkit-mask-position": maskPosition,
    },
    },
    React.createElement("div", null, "Hidden Text"),
    React.createElement("div", null, "Hidden Text"),
    React.createElement("div", null, "Hidden Text"),
    React.createElement("div", null, "Hidden Text")
    ),
    React.createElement("div", {
    className: "lens",
    style: {
    transform,
    },
    })
    );
    };
    const App = () => {
    const { orientation, requestAccess, revokeAccess, error } =
    useDeviceOrientation();
    const errorElement = error
    ? React.createElement("div", { className: "error" }, error.message)
    : null;
    return React.createElement(
    "div",
    { className: "box" },
    React.createElement(
    "button",
    { onClick: requestAccess },
    "Request For Gyro Access"
    ),
    errorElement,
    React.createElement(Demo, { orientation: orientation })
    );
    };
    ReactDOM.render(
    React.createElement(App, null),
    document.getElementById("root")
    );
    </script>
    </body>
    </html>


沪ICP备19023445号-2号
友情链接