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:
@@ -0,0 +1,95 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPersonaHover } from "./persona-hover";
|
||||
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
function setup() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const moves: number[] = [];
|
||||
const enter = vi.fn();
|
||||
const exit = vi.fn();
|
||||
canvas.addEventListener("mousemove", (event) => moves.push(event.clientX));
|
||||
canvas.addEventListener("mouseover", enter);
|
||||
canvas.addEventListener("mouseout", exit);
|
||||
return { hover: createPersonaHover(canvas), moves, enter, exit };
|
||||
}
|
||||
|
||||
describe("Persona hover timing", () => {
|
||||
it("positions the original reflection before entry and follows movement with inertia", () => {
|
||||
const { hover, moves } = setup();
|
||||
hover.enter({ clientX: 16, clientY: 32 });
|
||||
expect(moves).toEqual([16]);
|
||||
hover.move({ clientX: 48, clientY: 32 });
|
||||
expect(moves).toEqual([16]);
|
||||
vi.advanceTimersByTime(80);
|
||||
expect(moves.at(-1)).toBeGreaterThan(32);
|
||||
expect(moves.at(-1)).toBeLessThan(48);
|
||||
vi.advanceTimersByTime(800);
|
||||
expect(moves.at(-1)).toBe(48);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
hover.dispose();
|
||||
});
|
||||
|
||||
it("holds the last in-bounds position for 100ms before asking Rive to fade", () => {
|
||||
const { hover, moves, exit } = setup();
|
||||
hover.enter({ clientX: 16, clientY: 32 });
|
||||
hover.move({ clientX: 48, clientY: 32 });
|
||||
hover.leave();
|
||||
vi.advanceTimersByTime(99);
|
||||
expect(exit).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(exit).toHaveBeenCalledOnce();
|
||||
const count = moves.length;
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(moves).toHaveLength(count);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels a pending exit on quick re-entry without replaying the entrance", () => {
|
||||
const { hover, enter, exit } = setup();
|
||||
hover.enter({ clientX: 16, clientY: 32 });
|
||||
hover.leave();
|
||||
vi.advanceTimersByTime(60);
|
||||
hover.enter({ clientX: 48, clientY: 32 });
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(enter).toHaveBeenCalledOnce();
|
||||
expect(exit).not.toHaveBeenCalled();
|
||||
hover.dispose();
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("releases cancellation immediately and removes pending work on disposal", () => {
|
||||
const { hover, moves, exit } = setup();
|
||||
hover.enter({ clientX: 16, clientY: 32 });
|
||||
hover.leave();
|
||||
hover.cancel();
|
||||
expect(exit).toHaveBeenCalledOnce();
|
||||
hover.enter({ clientX: 48, clientY: 32 });
|
||||
hover.move({ clientX: 32, clientY: 32 });
|
||||
hover.leave();
|
||||
hover.dispose();
|
||||
const count = moves.length;
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(moves).toHaveLength(count);
|
||||
expect(exit).toHaveBeenCalledOnce();
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("continues from the visible reflection when re-entering during its fade", () => {
|
||||
const { hover, moves } = setup();
|
||||
hover.enter({ clientX: 16, clientY: 32 });
|
||||
hover.leave();
|
||||
vi.advanceTimersByTime(200);
|
||||
hover.enter({ clientX: 48, clientY: 32 });
|
||||
expect(moves.at(-1)).toBe(16);
|
||||
vi.advanceTimersByTime(80);
|
||||
expect(moves.at(-1)).toBeGreaterThan(16);
|
||||
expect(moves.at(-1)).toBeLessThan(48);
|
||||
hover.leave();
|
||||
vi.advanceTimersByTime(800);
|
||||
hover.enter({ clientX: 32, clientY: 32 });
|
||||
expect(moves.at(-1)).toBe(32);
|
||||
hover.dispose();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user