【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-ignoreCSS.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 secondsbutton.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.hqwc.cn/news/235797.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Python脚本实现图片合成PDF功能

目录 一、所需库 二、图片合成PDF的实现过程 三、完整的代码示例 四、注意事项 总结 在Python中&#xff0c;我们可以使用一些强大的库来实现图片合成PDF的功能。这个过程主要包括读取图片文件、将图片按照指定的顺序合并以及生成PDF文件。下面&#xff0c;让我们一起探索…

HX3002入耳检测光感驱动调试-感0x08 寄存器溢出,不变化错误问题解决方法

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)?可加我微信hezkz17, 本群提供音频技术答疑服务,+群赠送语音信号处理降噪算法,蓝牙耳机音频,DSP音频项目核心开发资料, 读取光感0x08 寄存器溢出,不变化错误问题?原因 原因:没有读取到0x08数据,没有读0x…

SS6811H38V/1.6A 两通道 H 桥驱动芯片

SS6811H 为舞台灯光和其它电机一体化应用 提供一种双通道集成电机驱动方案。SS6811H 有 两路 H 桥驱动&#xff0c;每个 H 桥可提供最大输出电流 1.6A (在 24V 和 Ta 25C 适当散热条件下)&#xff0c;可驱 动两个刷式直流电机&#xff0c;或者一个双极步进电机&#xff0c;或 …

ESP32-Web-Server编程- 使用表格(Table)实时显示设备信息

ESP32-Web-Server编程- 使用表格&#xff08;Table&#xff09;实时显示设备信息 概述 上节讲述了通过 Server-Sent Events&#xff08;以下简称 SSE&#xff09; 实现在网页实时更新 ESP32 Web 服务器的传感器数据。 本节书接上会&#xff0c;继续使用 SSE 机制在网页实时显…

【IEEE独立出版】2024第四届神经网络、信息与通信工程国际学术会议(NNICE 2024)

2024第四届神经网络、信息与通信工程国际学术会议&#xff08;NNICE 2024&#xff09; 2024 4th International Conference on Neural Networks, Information and Communication Engineering 2024第四神经网络、信息与通信工程国际学术会议&#xff08;NNICE 2024&#xff0…

力扣:1419. 数青蛙

题目&#xff1a; 代码&#xff1a; class Solution { public:int minNumberOfFrogs(string croakOfFrogs){string s "croak";int ns.size();//首先创建一个哈希表来标明每个元素出现的次数&#xff01;vector<int>hash(n); //不用真的创建一个hash表用一个数…

物流实时数仓ODS层——Mysql到Kafka

目录 1.采集流程 2.项目架构 3.resources目录下的log4j.properties文件 4.依赖 5.ODS层——OdsApp 6.环境入口类——CreateEnvUtil 7.kafka工具类——KafkaUtil 8.启动集群项目 这一层要从Mysql读取数据&#xff0c;分为事实数据和维度数据&#xff0c;将不同类型的数据…

学习记录PCL-1 通过哈希表进行三维点云的虚拟格网划分

直接对整个场景的点云进行特征提取&#xff0c;效果很差&#xff0c;因此通过划分区域格网进行划分。格网划分有很多种方式&#xff0c;在这里尝试使用哈希表进行格网链接&#xff0c;后续通过在每个格网内基于点云特征进行提取。 参考博客&#xff1a; 点云侠的PCL 点云分块_p…

数据库管理-第119期 记一次迁移和性能优化(202301130)

数据库管理-第119期 记一次迁移和性能优化&#xff08;202301130&#xff09; 1 迁移 之前因为DV组件没有迁移成功的那个PDB&#xff0c;后来想着在目标端安装DV组件迁移&#xff0c;结果目标端装不上&#xff0c;而且开了SR也没看出个所以然来。只能换一个方向&#xff0c;尝…

虚假IP地址攻击的溯源方法

随着网络技术的迅速发展&#xff0c;网络攻击行为也日益猖獗。其中&#xff0c;虚假IP地址攻击是一种较为常见的网络攻击方式&#xff0c;它利用虚假的IP地址&#xff0c;通过互联网对目标进行攻击和入侵。这种攻击方式不仅难以追踪&#xff0c;而且往往会给企业和个人带来巨大…

迭代实现二叉树的遍历

关卡名 理解树的前中后序遍历&#xff0c; 以及如何基于迭代方式实现 我会了✔️ 内容 1.迭代实现二叉树树的前序遍历 ✔️ 2.迭代实现二叉树的中序遍历 ✔️ 3.迭代实现二叉树的后序遍历 ✔️ 理论上&#xff0c;递归能做的迭代一定能做&#xff0c;但可能会比较复杂。上…

CentOS7.5搭建Hadoop-3.3.6集群的详细操作流程-实操版本

一、准备工作 1、安装 VMware&#xff0c;已安装的&#xff0c;跳过此步骤即可 官方正版VMware下载&#xff08;16 pro&#xff09;&#xff1a;https://www.aliyundrive.com/s/wF66w8kW9ac 安装&#xff1a;选一下安装地址&#xff0c;一直下一步即可。&#xff08;可能会要…