本文介绍: 水波纹点击特效 really cool,实现水波纹的方案也有很多,笔者经常使用 material 组件,非常喜欢 mui 中的 ripple,他家的 ripple 特效就是通过 css Houdini 实现的。今天我们将复刻一个 ripple,并封装hooks使用

水波纹点击特效 really cool,实现水波纹的方案也有很多,笔者经常使用 material 组件,非常喜欢 mui 中的 ripple,他家的 ripple 特效就是通过 css Houdini 实现的。
今天我们将复刻一个 ripple,并封装hooks使用

CSS Houdini

首先,我们需要了解下 CSS Houdini相关知识

Houdini一组底层 API,它们公开了 CSS 引擎各个部分,从而使开发人员能够通过加入浏览器渲染引擎样式布局过程扩展 CSS。Houdini一组 API,它们使开发人员可以直接访问CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器本地实现。
Houdini 的 CSS Typed OM 是一个包类型方法的 CSS 对象、并且暴露出了作为 JavaScript 对象的值。比起先前基于字符串的,对 HTMLElement.style 进行操作的方案,对 JavaScript 对象进行操作更符合直觉。每个元素样式表规则都拥有一个样式对应表,该对应可以通过 StylePropertyMap 来获得。

<script&gt;CSS.paintWorklet.addModule('csscomponent.js');</script>

csscomponents.js 里面定义一个 具名 类,然后用到元素即可

li {
  background-image: paint(myComponent, stroke, 10px);
  --highlights: blue;
  --lowlights: green;
}

一个 CSS Houdini 的特性就是 Worklet (en-US)。在它的帮助下,你可以通过引入一行 JavaScript 代码引入配置化的组件,从而创建模块式的 CSS。不依赖任何前置处理器、后置处理器或者 JavaScript 框架

没有明白?没事,直接实操就明白了。

实现思路

点击元素获取点击坐标(js 点击事件),将坐标颜色,时常等参数传递给 css 变量,并从坐标展开一个涟漪动画houdini worklet),worklet 获取参数渲染 canvas 动画即可
涟漪变化的相关参数时间--ripple-time 将会在后面的js点击事件实时更新

创建 ripple 绘制 worklet

注册一个名为 “ripple” 的 paint 类,获取涟漪动画css 变量然后渲染涟漪。

// ripple-worklet.js
try {
  registerPaint(
    "ripple",
    class {
      static get inputProperties() {
        return ["--ripple-x", "--ripple-y", "--ripple-color", "--ripple-time"];
      }
      paint(ctx, geom, properties) {
        const x = parseFloat(properties.get("--ripple-x").toString());
        const y = parseFloat(properties.get("--ripple-y").toString());
        const color = properties.get("--ripple-color").toString();
        const time = parseFloat(properties.get("--ripple-time").toString());

        ctx.fillStyle = color;
        ctx.globalAlpha = Math.max(1 - time, 0);
        ctx.arc(x, y, geom.width * time, 0, 2 * Math.PI);
        ctx.fill();
      }
    }
  );
} catch (error) {
  if (error.name !== "DOMException") {
    throw error;
  }
}

封装 useRipple hook

为简化使用,将点击事件,涟漪样式绑定ref 传递给需要使用涟漪的元素,并将应用 ripple worklet过程添加useRipple 内;useRipple 再设置一下传参,传递 color(涟漪层颜色), duration(涟漪时常)和 trigger(触发时机),用于提高涟漪的可定制能力。
其中,为了让动画持续更新,通过 requestAnimationFrame 递归调用 animate 函数实时更新 --ripple-time 参数

外部定义 isWorkletRegistered 标志,避免重复注册 ripple worklet.

import { useRef, useEffect } from "react";

export type RippleConfig = {
  color?: React.CSSProperties["color"];
  duration?: number;
  trigger?: "click" | "mousedown" | "pointerdown";
};

let isWorkletRegistered = false;

const useRipple = <T extends HTMLElement = HTMLButtonElement>(
  config: RippleConfig = {
    color: "rgba(31, 143, 255, 0.5)",
    duration: 500,
  }
): React.RefObject<T> => {
  const ref = useRef<T>(null);
  const mounted = useRef<boolean>(false);

  useEffect(() => {
    if (mounted.current) return;
    try {
      if ("paintWorklet" in CSS &amp;&amp; !isWorkletRegistered) {
        if (!isWorkletRegistered) {
          // @ts-ignore
          CSS.paintWorklet.addModule("houdini/ripple.js");
          isWorkletRegistered = true;
          console.log("Ripple worklet is registered");
        } else {
          console.warn("Ripple worklet is already registered");
        }
      } else {
        console.warn("Your browser doesn't support CSS Paint API");
      }
    } catch (error) {
      console.error(error);
    }
    mounted.current = true;
  }, []);

  useEffect(() => {
    const button = ref.current;
    if (!button) return;

    let animationFrameId: number | null = null;
    const handleClick = (event: MouseEvent) => {
      const rect = button.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;
      const startTime = performance.now();
      button.style.setProperty("--ripple-color", config.color ?? "rgba(31, 143, 255, 0.5)");
      button.style.setProperty("--ripple-x", `${x}px`);
      button.style.setProperty("--ripple-y", `${y}px`);
      button.style.setProperty("--ripple-time", "0");
      button.style.setProperty("background-image", "paint(ripple)");

      const animate = (time: number) => {
        const progress = (time - startTime) / (config.duration ?? 500); // Convert time to seconds
        button.style.setProperty("--ripple-time", `${progress}`);
        if (progress < 1) {
          animationFrameId = requestAnimationFrame(animate);
        } else {
          if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
          }
        }
      };

      animationFrameId = requestAnimationFrame(animate);
    };

    button.addEventListener(config.trigger ?? "mousedown", handleClick);

    return () => {
      if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
      button.removeEventListener(config.trigger ?? "mousedown", handleClick);
    };
  }, []);

  return ref;
};

export default useRipple;

ripple-worklet 转 Blob

上面的 ripple.js 我们只能放在 public 下或者公网地址,通过路径传给 CSS.paintWorklet.addModule,放在 useRipple 目录下通过"./ripple.js" 传是无效的。有没有解决办法呢?注意,这个路径其实是 URL,我们可以通过 URL.createObjectURL 封装 ripple.js,再传给 addModule:

// rippleWorklet.ts
const rippleWorklet = URL.createObjectURL(
  new Blob(
    [
      `try {
    registerPaint(
      "ripple",
      class {
        static get inputProperties() {
          return ["--ripple-x", "--ripple-y", "--ripple-color", "--ripple-time"];
        }
        paint(ctx, geom, properties) {
          const x = parseFloat(properties.get("--ripple-x").toString());
          const y = parseFloat(properties.get("--ripple-y").toString());
          const color = properties.get("--ripple-color").toString();
          const time = parseFloat(properties.get("--ripple-time").toString());
  
          ctx.fillStyle = color;
          ctx.globalAlpha = Math.max(1 - time, 0);
          ctx.arc(x, y, geom.width * time, 0, 2 * Math.PI);
          ctx.fill();
        }
      }
    );
  } catch (error) {
    if (err.name !== "DOMException") {
      throw err;
    }
  }`,
    ],
    {
      type: "application/javascript",
    }
  )
);

export default rippleWorklet;

然后调整 useRipple:

CSS.paintWorklet.addModule(rippleWorklet); // "Houdini/ripple.js"

此时效果是一样的,不再需要额外配置 ripple.js.

使用示例

以下代码用 useRipple 创建了一个附带 ripple 特效div 组件,你可以用相同的方式任意元素添加 ripple,也可以直接用这个 Ripple 组件包裹其他元素

import { useRipple } from "@/hooks";

export default Ripple() {
  const rippleRef = useRipple<HTMLDivElement>();
  return(
    <div ref={rippleRef}>水波纹特效</div>
  )
}

结合 useRipple 高仿 @mui/Button 的效果
涟漪按钮效果

.confirm-modal__actions__button--cancel {
    color: dodgerblue;
}

.confirm-modal__actions__button--confirm {
    color: #fff;
    background-color: dodgerblue;
}

.confirm-modal__actions__button {
    border-radius: 4px;
    margin-left: 0.5rem;
    text-transform: uppercase;
    font-size: 12px;
}

Bingo! 一个便捷的 useRipple 就这样实现了!

原文地址:https://blog.csdn.net/m0_51810668/article/details/134680448

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_18617.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注