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

2023-11-30 17:52

本文主要是介绍【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 就这样实现了!

这篇关于【raect.js + hooks】useRef 搭配 Houdini 创造 useRipple的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/437974

相关文章

JS+HTML实现在线图片水印添加工具

《JS+HTML实现在线图片水印添加工具》在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题,本文将实现一个完全基于HTML+CSS构建的现代化图片水印在线工具... 目录概述功能亮点使用方法技术解析延伸思考运行效果项目源码下载总结概述在社交媒体和内容创作日益频繁的

Node.js 数据库 CRUD 项目示例详解(完美解决方案)

《Node.js数据库CRUD项目示例详解(完美解决方案)》:本文主要介绍Node.js数据库CRUD项目示例详解(完美解决方案),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考... 目录项目结构1. 初始化项目2. 配置数据库连接 (config/db.js)3. 创建模型 (models/

使用Node.js制作图片上传服务的详细教程

《使用Node.js制作图片上传服务的详细教程》在现代Web应用开发中,图片上传是一项常见且重要的功能,借助Node.js强大的生态系统,我们可以轻松搭建高效的图片上传服务,本文将深入探讨如何使用No... 目录准备工作搭建 Express 服务器配置 multer 进行图片上传处理图片上传请求完整代码示例

用js控制视频播放进度基本示例代码

《用js控制视频播放进度基本示例代码》写前端的时候,很多的时候是需要支持要网页视频播放的功能,下面这篇文章主要给大家介绍了关于用js控制视频播放进度的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言html部分:JavaScript部分:注意:总结前言在javascript中控制视频播放

Node.js net模块的使用示例

《Node.jsnet模块的使用示例》本文主要介绍了Node.jsnet模块的使用示例,net模块支持TCP通信,处理TCP连接和数据传输,具有一定的参考价值,感兴趣的可以了解一下... 目录简介引入 net 模块核心概念TCP (传输控制协议)Socket服务器TCP 服务器创建基本服务器服务器配置选项服

mac安装nvm(node.js)多版本管理实践步骤

《mac安装nvm(node.js)多版本管理实践步骤》:本文主要介绍mac安装nvm(node.js)多版本管理的相关资料,NVM是一个用于管理多个Node.js版本的命令行工具,它允许开发者在... 目录NVM功能简介MAC安装实践一、下载nvm二、安装nvm三、安装node.js总结NVM功能简介N

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2

JS 实现复制到剪贴板的几种方式小结

《JS实现复制到剪贴板的几种方式小结》本文主要介绍了JS实现复制到剪贴板的几种方式小结,包括ClipboardAPI和document.execCommand这两种方法,具有一定的参考价值,感兴趣的... 目录一、Clipboard API相关属性方法二、document.execCommand优点:缺点:

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

使用Vue.js报错:ReferenceError: “Vue is not defined“ 的原因与解决方案

《使用Vue.js报错:ReferenceError:“Vueisnotdefined“的原因与解决方案》在前端开发中,ReferenceError:Vueisnotdefined是一个常见... 目录一、错误描述二、错误成因分析三、解决方案1. 检查 vue.js 的引入方式2. 验证 npm 安装3.