【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常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏

使用JS/Jquery获得父窗口的几个方法(笔记)

<pre name="code" class="javascript">取父窗口的元素方法:$(selector, window.parent.document);那么你取父窗口的父窗口的元素就可以用:$(selector, window.parent.parent.document);如题: $(selector, window.top.document);//获得顶级窗口里面的元素 $(

js异步提交form表单的解决方案

1.定义异步提交表单的方法 (通用方法) /*** 异步提交form表单* @param options {form:form表单元素,success:执行成功后处理函数}* <span style="color:#ff0000;"><strong>@注意 后台接收参数要解码否则中文会导致乱码 如:URLDecoder.decode(param,"UTF-8")</strong></span>

js react 笔记 2

起因, 目的: 记录一些 js, react, css 1. 生成一个随机的 uuid // 需要先安装 crypto 模块const { randomUUID } = require('crypto');const uuid = randomUUID();console.log(uuid); // 输出类似 '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

uuid.js 使用

相关代码 import { NIL } from "uuid";/** 验证UUID* 为空 则返回 false* @param uuid* @returns {boolean}*/export function MyUUIDValidate(uuid: any): boolean {if (typeof uuid === "string" && uuid !== NIL) { //uuid

js定位navigator.geolocation

一、简介   html5为window.navigator提供了geolocation属性,用于获取基于浏览器的当前用户地理位置。   window.navigator.geolocation提供了3个方法分别是: void getCurrentPosition(onSuccess,onError,options);//获取用户当前位置int watchCurrentPosition(