feat: 添加交互状态插件,增加悬停和移动状态回调
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@leafer-in/animate": "^2.1.8",
|
||||
"@leafer-in/color": "^2.1.8",
|
||||
"@leafer-in/state": "^2.1.8",
|
||||
"@leafer-ui/core": "^2.1.8",
|
||||
"@leafer-ui/draw": "^2.1.8",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
|
||||
Generated
+16
@@ -14,6 +14,9 @@ importers:
|
||||
'@leafer-in/color':
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8(@leafer-in/interface@2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8))(@leafer-ui/draw@2.1.8)(@leafer-ui/interface@2.1.8)
|
||||
'@leafer-in/state':
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8(@leafer-in/interface@2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8))(@leafer-ui/core@2.1.8)(@leafer-ui/interface@2.1.8)
|
||||
'@leafer-ui/core':
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8
|
||||
@@ -247,6 +250,13 @@ packages:
|
||||
'@leafer-ui/interface': ^2.1.8
|
||||
'@leafer/interface': ^2.1.8
|
||||
|
||||
'@leafer-in/state@2.1.8':
|
||||
resolution: {integrity: sha512-YHz1dg3VaTG74y9/GWhbiJ9OsUjv1CQxMxCRABFYAY0bO8PqUDYw82Wk4eoVCYIHXK3olr9vnQpyvmjUMn7rTw==}
|
||||
peerDependencies:
|
||||
'@leafer-in/interface': ^2.1.8
|
||||
'@leafer-ui/core': ^2.1.8
|
||||
'@leafer-ui/interface': ^2.1.8
|
||||
|
||||
'@leafer-ui/app@2.1.8':
|
||||
resolution: {integrity: sha512-oOxmObO1TEfm4ZVJP0k95BU42MuYh4Que8PkJs5LKxIgofqmS1kF5SGGkbGBM/5he8KGVzsYQJXPC2MiFkqJ9Q==}
|
||||
|
||||
@@ -1431,6 +1441,12 @@ snapshots:
|
||||
'@leafer-ui/interface': 2.1.8
|
||||
'@leafer/interface': 2.1.8
|
||||
|
||||
'@leafer-in/state@2.1.8(@leafer-in/interface@2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8))(@leafer-ui/core@2.1.8)(@leafer-ui/interface@2.1.8)':
|
||||
dependencies:
|
||||
'@leafer-in/interface': 2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8)
|
||||
'@leafer-ui/core': 2.1.8
|
||||
'@leafer-ui/interface': 2.1.8
|
||||
|
||||
'@leafer-ui/app@2.1.8':
|
||||
dependencies:
|
||||
'@leafer-ui/data': 2.1.8
|
||||
|
||||
@@ -71,6 +71,11 @@ function App() {
|
||||
fill: item.color,
|
||||
cornerRadius: 16,
|
||||
opacity: entering ? 0 : 1,
|
||||
around: "center",
|
||||
hoverStyle: {
|
||||
scale: 1.05,
|
||||
opacity: 0.7
|
||||
},
|
||||
children: [
|
||||
new Text({
|
||||
width: size.width,
|
||||
@@ -128,6 +133,10 @@ function App() {
|
||||
draggable={ready}
|
||||
background="#171717"
|
||||
onCardClick={(item) => console.log("clicked", item)}
|
||||
// onCardHover={(item, _index, hovering) =>
|
||||
// console.log(hovering ? "hover in" : "hover out", item)
|
||||
// }
|
||||
onMovingChange={(moving) => console.log("moving", moving)}
|
||||
onVisibleCardsChange={handleVisibleChange}
|
||||
/>
|
||||
{/* 实时展示可视卡片:可视度 + 左上角坐标 */}
|
||||
|
||||
@@ -61,6 +61,16 @@ export interface InfiniteCanvasProps<T> {
|
||||
friction?: number;
|
||||
/** 点击卡片回调 */
|
||||
onCardClick?: (item: T, index: number) => void;
|
||||
/**
|
||||
* 悬停卡片回调:指针进入时 hovering=true,移出时 hovering=false
|
||||
* 同一张卡片因无限平铺可能存在多个实例,进出各实例都会触发
|
||||
*/
|
||||
onCardHover?: (item: T, index: number, hovering: boolean) => void;
|
||||
/**
|
||||
* 移动状态回调:拖拽或惯性滑动开始时 moving=true,完全静止后 moving=false
|
||||
* 仅在状态翻转时触发一次(拖拽接惯性视为持续移动,中途不会触发 false)
|
||||
*/
|
||||
onMovingChange?: (moving: boolean) => void;
|
||||
/**
|
||||
* 可视区域卡片变化回调:每当可视卡片集合或其可视度发生变化时触发
|
||||
* 内部用 requestAnimationFrame 合帧、并对结果做去重,空闲时零开销
|
||||
|
||||
@@ -26,6 +26,8 @@ export function useInfiniteSlide<T>(
|
||||
// 易变的回调/配置用 ref 存最新值,避免进入 effect 依赖导致画布重建
|
||||
// 在 passive effect 中更新(不能在 render 期间写 ref)
|
||||
const onCardClickRef = useRef(props.onCardClick);
|
||||
const onCardHoverRef = useRef(props.onCardHover);
|
||||
const onMovingChangeRef = useRef(props.onMovingChange);
|
||||
const onVisibleCardsChangeRef = useRef(props.onVisibleCardsChange);
|
||||
const visibilityThresholdRef = useRef(props.visibilityThreshold ?? 0);
|
||||
const draggableRef = useRef(draggable);
|
||||
@@ -43,6 +45,8 @@ export function useInfiniteSlide<T>(
|
||||
|
||||
useEffect(() => {
|
||||
onCardClickRef.current = props.onCardClick;
|
||||
onCardHoverRef.current = props.onCardHover;
|
||||
onMovingChangeRef.current = props.onMovingChange;
|
||||
onVisibleCardsChangeRef.current = props.onVisibleCardsChange;
|
||||
visibilityThresholdRef.current = props.visibilityThreshold ?? 0;
|
||||
draggableRef.current = draggable;
|
||||
@@ -222,6 +226,13 @@ export function useInfiniteSlide<T>(
|
||||
node.on(PointerEvent.TAP, () =>
|
||||
onCardClickRef.current?.(item, index)
|
||||
);
|
||||
// 指针进入/移出:用于 hover 回调(拖拽中也会触发,由使用方自行判断)
|
||||
node.on(PointerEvent.ENTER, () =>
|
||||
onCardHoverRef.current?.(item, index, true)
|
||||
);
|
||||
node.on(PointerEvent.LEAVE, () =>
|
||||
onCardHoverRef.current?.(item, index, false)
|
||||
);
|
||||
leafer.add(node);
|
||||
nodes.push(node);
|
||||
meta.push({ item, index });
|
||||
@@ -256,6 +267,37 @@ export function useInfiniteSlide<T>(
|
||||
let vy = 0;
|
||||
let raf = 0;
|
||||
|
||||
// 移动状态(拖拽 ∪ 惯性):仅在翻转时回调一次,拖拽接惯性不会中途回 false
|
||||
let moving = false;
|
||||
const setMoving = (next: boolean) => {
|
||||
if (moving === next) return;
|
||||
moving = next;
|
||||
onMovingChangeRef.current?.(next);
|
||||
};
|
||||
|
||||
// 按住但停住不动的空闲检测:超过 IDLE_MS 没有新的位移就判定静止
|
||||
// (浏览器在指针不动时不再派发 move 事件,否则 moving 会一直卡在 true)
|
||||
const IDLE_MS = 150;
|
||||
let idleTimer: ReturnType<typeof setTimeout> | 0 = 0;
|
||||
const clearIdle = () => {
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = 0;
|
||||
}
|
||||
};
|
||||
const armIdle = () => {
|
||||
clearIdle();
|
||||
idleTimer = setTimeout(() => {
|
||||
idleTimer = 0;
|
||||
// 仍按着但已停住:置静止,并清掉残留速度避免松手误触发惯性
|
||||
if (dragging) {
|
||||
vx = 0;
|
||||
vy = 0;
|
||||
setMoving(false);
|
||||
}
|
||||
}, IDLE_MS);
|
||||
};
|
||||
|
||||
// 停止惯性滑动(取消 requestAnimationFrame)
|
||||
const stopInertia = () => {
|
||||
if (raf) {
|
||||
@@ -272,6 +314,7 @@ export function useInfiniteSlide<T>(
|
||||
vy *= frictionRef.current;
|
||||
if (Math.abs(vx) < MIN && Math.abs(vy) < MIN) {
|
||||
raf = 0;
|
||||
setMoving(false); // 惯性自然衰减到静止
|
||||
return;
|
||||
}
|
||||
shift(vx, vy);
|
||||
@@ -290,6 +333,7 @@ export function useInfiniteSlide<T>(
|
||||
vx = 0;
|
||||
vy = 0;
|
||||
container.style.cursor = "grabbing"; // 按下:抓握手势
|
||||
// 注意:按下还未移动不算「移动中」,moving 留到 onMove 真正位移时再置 true
|
||||
};
|
||||
|
||||
const onMove = (e: PointerEvent) => {
|
||||
@@ -300,15 +344,22 @@ export function useInfiniteSlide<T>(
|
||||
lastY = e.y;
|
||||
vx = dx;
|
||||
vy = dy;
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
setMoving(true); // 真正产生位移才算移动中
|
||||
armIdle(); // 重置空闲计时:停住不动到点后会判定静止
|
||||
}
|
||||
shift(dx, dy);
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
clearIdle(); // 松手即结束拖拽,停掉空闲计时
|
||||
container.style.cursor = "grab"; // 松开:恢复可抓握手势
|
||||
if (inertiaRef.current && (Math.abs(vx) > 0.5 || Math.abs(vy) > 0.5)) {
|
||||
startInertia();
|
||||
startInertia(); // 拖拽接惯性,保持 moving=true
|
||||
} else {
|
||||
setMoving(false); // 无惯性,松手即静止
|
||||
}
|
||||
};
|
||||
|
||||
@@ -325,6 +376,7 @@ export function useInfiniteSlide<T>(
|
||||
layoutRef.current = undefined;
|
||||
ro.disconnect();
|
||||
stopInertia();
|
||||
clearIdle();
|
||||
if (visRaf) cancelAnimationFrame(visRaf);
|
||||
leafer.off(PointerEvent.DOWN, onDown);
|
||||
leafer.off(PointerEvent.MOVE, onMove);
|
||||
|
||||
+4
-3
@@ -1,8 +1,9 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
// Leafer 插件按副作用导入:把 animate()/颜色能力挂到节点上(仅安装不导入不生效)
|
||||
import "@leafer-in/animate";
|
||||
import "@leafer-in/color";
|
||||
// Leafer 插件
|
||||
import "@leafer-in/animate"; // 动画插件
|
||||
import "@leafer-in/color"; // 颜色插件(提供 Color 类、渐变、调色板等)
|
||||
import "@leafer-in/state"; // 交互状态插件
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user