fix: smooth persona hover transitions
Preserve the original reflection material while adding pointer inertia and a short exit hold. Retain the Rive pointer constraint through the fade and blend rapid re-entry to avoid abrupt changes. Cover timing, re-entry, cancellation, renderer disposal and native Rive frames. The fade regression fails with the previous resource and passes with the updated resource.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import type { RiveParameters } from "@rive-app/react-webgl2";
|
||||
import { EventType, useRive, useStateMachineInput } from "@rive-app/react-webgl2";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import type { PointerEvent as ReactPointerEvent } from "react";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { loadPersonaResource } from "./persona-resource";
|
||||
import { createPersonaHover } from "./persona-hover";
|
||||
|
||||
const STATE_MACHINE = "default";
|
||||
|
||||
@@ -69,6 +70,17 @@ export const Persona = memo(function Persona({
|
||||
const thinkingInput = useStateMachineInput(rive, STATE_MACHINE, "thinking");
|
||||
const speakingInput = useStateMachineInput(rive, STATE_MACHINE, "speaking");
|
||||
const asleepInput = useStateMachineInput(rive, STATE_MACHINE, "asleep");
|
||||
const hoverRef = useRef<ReturnType<typeof createPersonaHover> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvas) return;
|
||||
const hover = createPersonaHover(canvas);
|
||||
hoverRef.current = hover;
|
||||
return () => {
|
||||
hover.dispose();
|
||||
hoverRef.current = null;
|
||||
};
|
||||
}, [canvas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listeningInput) {
|
||||
@@ -85,24 +97,21 @@ export const Persona = memo(function Persona({
|
||||
}
|
||||
}, [state, listeningInput, thinkingInput, speakingInput, asleepInput]);
|
||||
|
||||
const forwardPointer = (event: ReactPointerEvent<HTMLSpanElement>, type: "mouseover" | "mousemove" | "mouseout") => {
|
||||
const forwardPointer = (event: ReactPointerEvent<HTMLSpanElement>, type: "enter" | "move" | "leave" | "cancel") => {
|
||||
if (!event.isPrimary || event.pointerType === "touch") return;
|
||||
// Let Rive transform CSS coordinates into artboard coordinates and update
|
||||
// its original pointer constraint. Only forward hover events, never presses
|
||||
// or touch-generated mouse events, so mobile taps retain their button action.
|
||||
canvas?.dispatchEvent(new MouseEvent(type, {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY
|
||||
}));
|
||||
// its original pointer constraint after smoothing CSS coordinates. Only
|
||||
// hover events enter this path, so mobile taps retain their button action.
|
||||
hoverRef.current?.[type](event);
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("relative block size-16 shrink-0", className)}
|
||||
onPointerEnter={(event) => forwardPointer(event, "mouseover")}
|
||||
onPointerMove={(event) => forwardPointer(event, "mousemove")}
|
||||
onPointerLeave={(event) => forwardPointer(event, "mouseout")}
|
||||
onPointerCancel={(event) => forwardPointer(event, "mouseout")}
|
||||
onPointerEnter={(event) => forwardPointer(event, "enter")}
|
||||
onPointerMove={(event) => forwardPointer(event, "move")}
|
||||
onPointerLeave={(event) => forwardPointer(event, "leave")}
|
||||
onPointerCancel={(event) => forwardPointer(event, "cancel")}
|
||||
>
|
||||
<RiveComponent className="pointer-events-none size-full" />
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user