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

news2024/10/6 19:20:43

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vuepress-----3、导航栏

3、导航栏 # 页面目录结构约定 . ├── docs │ ├── .vuepress (可选的) │ │ ├── components (可选的) │ │ ├── theme (可选的) │ │ │ └── Layout.vue │ │ ├── public (可选的) │ │ ├── styles (可选的) │ │ │…

《微信小程序开发从入门到实战》学习三十七

4.2 云开发JSON数据库 4.2.8 分页查询 在计算机互联网时代&#xff0c;很多页面底部分页导航按钮&#xff0c;有首页、上一页、第一页、第二页、尾页。 分页查询是指根据页码将每一页的数据查询出来。 在移动互联网时代&#xff0c;网页和应用都对网页进行优化&#xff0c;…

PHP在线日语学习平台

有需要请加文章底部Q哦 可远程调试 PHP在线日语学习平台 一 介绍 此日语学习平台基于原生PHP开发&#xff0c;数据库mysql。系统角色分为用户和管理员。(附带参考设计文档) 技术栈&#xff1a;phpmysqlphpstudyvscode 二 功能 学生 1 注册/登录/注销 2 个人中心 3 查看课程…

Java小游戏 王者荣耀(简易版)

GameFrame类 所需图片&#xff1a; package 王者荣耀;import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.File; import java.util.ArrayList…

轻型载重汽车转向前桥总成系统毕业设计机械设计

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;前桥 获取完整说明报告工程源文件 绪论 1.1 轻型载重汽车转向桥的设计意义 汽车是现代交通工具中用得最多&#xff0c;最普遍&#xff0c;也是最方便的交通运输工具。汽车转向系是汽车上的一个重要系统,它是汽车转向运动…

基于STC12C5A60S2系列1T 8051单片机的IIC总线器件24C02实现掉电保存计时时间应用

基于STC12C5A60S2系列1T 8051单片机的IIC总线器件24C02实现掉电保存计时时间应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍液晶显示器LCD1602简单介绍IIC通信简单…

用行云管家实现IT统一运维管理,提高运维效率

随着公司业务的不断壮大&#xff0c;需要用到的IT系统也越来越多&#xff0c;使用起来耗时耗力。因此实现IT统一运维管理已成为提高运维效率、降低成本、优化资源配置的重要途径。这里我们小编告诉您&#xff0c;用行云管家实现IT统一运维管理&#xff0c;提高运维效率&#xf…

java元注解

一、注解 Annotation&#xff08;注解&#xff09;是 Java 提供的一种对元程序中元素关联信息和元数据&#xff08;metadata&#xff09;的途径和方法。 Annatation(注解)是一个接口&#xff0c;程序可以通过反射来获取指定程序中元素的 Annotation对象&#xff0c;然后通过该…

焊接设备行业分析:预计2029年将达到834亿元

近年来我国焊割设备行业的主要出口产品多为零部件以及部分中、低端设备&#xff0c;其出口单价和利润额均相对较低。 随着国内原材料价格上涨和人民币不断升值&#xff0c;出口产品的竞争力日趋下降&#xff0c;利润空间也随着出口价格的下降被进一步压缩。同时近年来国际经济形…

猫头虎博主与CSDN的三年之约——我的创作纪念日三周年

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

数学建模-基于脑出血患者院前指标的多种机器学习预测模型构建及比较研究

基基于脑出血患者院前指标的多种机器学习预测模型构建及比较研究 整体求解过程概述(摘要) 卒中是全球致残率、致死率最高的疾病之一&#xff0c;其中脑出血&#xff08;Intracerebral hemorrhage&#xff0c;ICH&#xff09;患者仅占卒中患者的 20%&#xff0c;但致残、死亡人…

Retrobatch for mac图片批处理软件

Retrobatch是一款功能强大的图片批量操作软件&#xff0c;提供了批量加水印、裁剪、压缩等功能&#xff0c;而且处理速度非常快。 在Retrobatch中&#xff0c;用户可以通过拖动相应动作到工作区形成节点(Nodes)&#xff0c;并将节点连接起来形成一个Workflow&#xff0c;最后运…

SpringBoot application.yml配置文件写法

1&#xff0c;基本介绍 &#xff08;1&#xff09;YAML 是 JSON 的超集&#xff0c;简洁而强大&#xff0c;是一种专门用来书写配置文件的语言&#xff0c;可以替代 application.properties。 &#xff08;2&#xff09;在创建一个 SpringBoot 项目时&#xff0c;引入的 spri…

Windows系统搭建Appium 2 和 Appium Inspector 环境

前言 自 2022 年 1 月 1 日起&#xff0c;Appium 核心团队不再维护 Appium 1.x。官方支持的平台驱动程序的所有最新版本均不兼容 Appium 1.x&#xff0c;需要 Appium 2 才能运行。 Appium 2是一个自动化移动应用程序的开源工具&#xff0c;它带来了以下重要改进&#xff1a;  …

【Linux】进程控制--进程创建/进程终止/进程等待/进程程序替换/简易shell实现

文章目录 一、进程创建1.fork函数2.fork函数返回值3.写时拷贝4.fork常规用法5.fork调用失败的原因 二、进程终止1.进程退出码2.进程退出场景3.进程常见退出方法 三、进程等待1.为什么要进行进程等待2.如何进行进程等待1.wait方法2.waitpid方法3.获取子进程status4.进程的阻塞等…

对接电商平台高效获取各大电商平台数据,商品详情数据代码示例

电商可以通过使用API接口获取商品信息数据。API是应用程序编程接口的缩写&#xff0c;它允许程序之间进行通信和数据传输。为了获取商品信息数据&#xff0c;电商可以利用API接口向商品供应商的数据库发送请求&#xff0c;然后接收并解析返回的数据。 具体来说&#xff0c;电商…

高压放大器用于什么地方使用

高压放大器是一种电子设备&#xff0c;用于将低电压信号放大到高电压水平。它主要应用于需要高电压的领域&#xff0c;下面将详细介绍其主要应用地方。 科研实验室&#xff1a;科研实验室通常需要进行高电压实验&#xff0c;例如粒子加速器、高压放电实验等。高压放大器可以为这…

uniapp和vue3+ts实现自定义头部导航栏左侧胶囊内容

由于某些原因&#xff0c;可能需要我们自己定义头部导航栏的内容&#xff0c;实现各种设计师画的设计稿&#xff0c;所以就需要这个自定义的组件&#xff0c;实现的内容&#xff1a;自定义标题和左侧胶囊图标内容&#xff0c;也可以自定义搜索内容到里面&#xff0c;实现的效果…

网络和Linux网络_7(传输层)UDP和TCP协议(端口号+确认应答+超时重传+三次握手四次挥手)

目录 1. 重看端口号 1.1 端口号的概念 1.2 端口号的划分 2. 重看UDP协议 2.1 UDP协议格式 2.2 UDP的特点 3. 重看TCP协议 3.1 TCP协议格式 3.2 TCP的解包分用 3.3 TCP的可靠性及机制 3.3.1 确认应答ACK机制 3.3.2 超时重传机制 3.3.3 连接管理机制&#xff08;三次…

HCIP-十三、BGP综合选路

十三、BGP综合选路 实验拓扑实验需求及解法1.预配置包括2.在所有设备的系统视图下配置全局router id为Loopback0地址。3.内部网关协议IGP4.建立BGP邻居5.发布BGP路由6.协议优先级选路7. 权重&#xff08;PrefVal&#xff09;选路8.本地优先&#xff08;LocPrf&#xff09;选路9…