【raect.js + hooks】useRef 搭配 Houdini 创造 useRipple

水波纹点击特效 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>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 && !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 就这样实现了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/205807.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue3 router-view 使用keep-alive报错parentcomponent.ctx.deactivate is not a function

问题 如下图&#xff0c;在component组件上添加v-if判断&#xff0c;会报错: parentcomponent.ctx.deactivate is not a function 解决方法 去除v-if&#xff0c;将key直接添加上。由于有的公用页面&#xff0c;需要刷新&#xff0c;不希望缓存&#xff0c;所以需要添加key…

【23真题】快跑,考太偏了这所211!

今天分享的是23年湖南师范997的信号与系统试题及解析。 小马哥Tips&#xff1a; 本套试卷难度分析&#xff1a;22年湖南师范997考研真题&#xff0c;我也发布过&#xff0c;若有需要&#xff0c;戳这里自取&#xff01;本套试题难度中等&#xff0c;题量适中&#xff0c;但是…

升级python后sudo apt-get update报错

sudo apt-get update 报错&#xff1a; sh: /usr/lib/cnf-update-db: /usr/bin/python3.7.5: bad interpreter: No such file or directory Reading package lists... Done E: Problem executing scripts APT::Update::Post-Invoke-Success if /usr/bin/test -w /var/lib/c…

CANDENCE: PCB 如何高亮网络、器件

PCB 如何高亮网络、器件 开始前先学习一个单词&#xff1a;assign CANDECE 高亮网络 step1: 选择一个颜色&#xff1a;红色 step2: 筛选要高亮什么&#xff1a;网络 or 器件&#xff0c;这里选择网络。 step3&#xff1a;鼠标点击要高亮的网络&#xff1a; 这里是GND 这里…

罐装葡萄酒会成为主流吗?

许多人认为罐装葡萄酒可能是葡萄酒行业的下一个大事件&#xff0c;一个有待提出的问题&#xff0c;罐装葡萄酒会成为主流吗&#xff1f;来自云仓酒庄品牌雷盛红酒分享还是这种形式的基础永远会限制它的吸引力&#xff1f;在这里&#xff0c;我们一起来探讨支持和反对罐装葡萄酒…

力扣题:单词-11.20

力扣题-11.20 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;58. 最后一个单词的长度 解题思想&#xff1a;按空格划分&#xff0c;然后统计单词长度即可 class Solution(object):def lengthOfLastWord(self, s):""":type s: str…

Java——TreeSet用法

Java——TreeSet TreeSet 是 Java 中的一个有序集合类&#xff0c;它基于红黑树&#xff08;Red-Black Tree&#xff09;实现。 下面详细介绍 TreeSet 的用法和特点&#xff1a; 有序性&#xff1a;TreeSet 中的元素按照自然顺序或者通过自定义的比较器进行排序。它保证了元素…

网工内推 | 云计算运维,云相关认证优先,最高30K,带薪年假

01 安畅网络 招聘岗位&#xff1a;云计算运维工程师 职责描述&#xff1a; 1、负责对公有云平台的计算、存储、网络资源等IAAS/SAAS/PAAS层产品组件日常交付部署运维工作&#xff0c;包括调试、配置、维护、监控、优化等工作&#xff1b; 2、负责对操作系统及应用日常运行维护…

OSG编程指南<十七>:OSG光照与材质

1、OSG光照 OSG 全面支持 OpenGL 的光照特性&#xff0c;包括材质属性&#xff08;material property&#xff09;、光照属性&#xff08;light property&#xff09;和光照模型&#xff08;lighting model&#xff09;。与 OpenGL 相似&#xff0c;OSG 中的光源也是不可见的&a…

java拦截器,过滤器,监听器的区别

拦截器与过滤器 1&#xff1a;过滤器 过滤器主要作用在请求到达Servlet之前&#xff0c;对请求进行预处理&#xff0c;可以对HTTP请求进行过滤、修改。过滤器通常用于日志记录、字符编码转换、权限检查等任务。过滤器是基于回调函数实现的&#xff0c;重写doFilter()方法实现过…

AT89S52单片机智能寻迹小车自动红外避障趋光检测发声发光设计

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;寻迹 获取完整说明报告源程序数据 小车具有以下几个功能&#xff1a;自动避障功能&#xff1b;寻迹功能&#xff08;按路面的黑色轨道行驶&#xff09;&#xff1b;趋光功能&#xff08;寻找前方的点光源并行驶到位&…

MatchPyramid实现文本匹配

引言 今天利用MatchPyramid实现文本匹配。 原论文解析→点此←。 MatchPyramid 核心思想是计算两段文本间的匹配矩阵&#xff0c;把它当成一个图形利用多层卷积网络提取不同层级的交互模式。 匹配矩阵是通过计算两段输入文本基本单元(比如字或词)之间相似度得到的&#xf…

windows系统bat脚本命令总结之EnableDelayedExpansion

前言 做了一段时间的bat脚本开发&#xff0c;bat脚本中有各种各样的命令跟传统的编程逻辑完全不同&#xff0c;本专栏会讲解下各种各式的命令使用方法。 本篇文章讲解的是EnableDelayedExpansion的使用。 EnableDelayedExpansion简介 EnableDelayedExpansion是用于在批处理脚本…

Linux:理解文件重定向

文章目录 文件内核对象fd的分配问题重定向的现象dup2 重定向的使用标准输出和标准错误 前面对于文件有了基本的认知&#xff0c;那么基于前面的认知&#xff0c;本篇总结的是文件重定向的含义极其本质 文件内核对象 首先理解一下file内核对象是什么&#xff0c;回顾一下下面这…

python-nmap库使用教程(Nmap网络扫描器的Python接口)(功能:主机发现、端口扫描、操作系统识别等)

文章目录 Python-nmap库使用教程前置条件引入python-nmap创建Nmap扫描实例执行简单的主机发现&#xff08;nmap -sn&#xff09;示例&#xff0c;我有一台主机配置为不响应 ICMP 请求&#xff0c;但使用nmap -sn&#xff0c;仍然能够探测到设备&#xff1a; 端口扫描扫描特定端…

从setText处理来学习绘制流程

Android中TextView调用setText是会进行text文字的更新&#xff0c;是一个比较简单的画面变化&#xff0c;这可以作为一个出发点来查看绘制处理流程。这里来问问chatGPT&#xff0c;来查看大致流程 请讲讲Android中textView的setText处理流程 ChatGPT Poe 当你调用 textView.s…

二分算法(整数二分、浮点数二分)

文章目录 二分一、整数二分&#xff08;一&#xff09;整数二分思路&#xff08;二&#xff09;整数二分算法模板1.左查找&#xff08;寻找左侧边界&#xff09;2.右查找&#xff08;寻找右侧边界&#xff09;3.总模板 &#xff08;三&#xff09;题目&#xff1a;数的范围 二、…

【linux网络】补充网关服务器搭建,综合应用SNAT、DNAT转换,dhcp分配、dns分离解析,nfs网络共享以及ssh免密登录

目录 linux网络的综合应用 1&#xff09;网关服务器&#xff1a;ens35&#xff1a;12.0.0.254/24&#xff0c;ens33&#xff1a;192.168.100.254/24&#xff1b;Server1&#xff1a;192.168.100.101/24&#xff1b;PC1和server2&#xff1a;自动获取IP&#xff1b;交换机无需…

spring框架的事务传播级别经典篇

一 spring事务传播级别 1.1 总结概述 方法A:外围方法&#xff0c;方法B&#xff1a;内部方法&#xff0c;在A中调用B 1.事务级别PROPAGATION_REQUIRED&#xff1a; 如果A为PROPAGATION_REQUIRED&#xff1a;B 不管有没有设置事务级别&#xff0c;都会加入到A的事务级别中。如…

低代码究竟有何特别之处?为什么很多企业倾向于用低代码开发软件?

目录 一、低代码是什么 二、低代码有哪些核心能力&#xff1f; 三、低代码能做哪些事情&#xff1f; 1、软件开发快效率高 2、满足企业的多样化需求 3、轻松与异构系统集成 4、软件维护成本低 5、为企业实现降本增效 四、结语 低代码平台正高速发展中&#xff0c;越来越多的企业…