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:
@@ -44,9 +44,10 @@ window.ready = new Promise(resolve => RuntimeLoader.getInstance(resolve)).then(r
|
||||
context.clearRect(0, 0, 192, 192);
|
||||
context.drawImage(canvas, 0, 0);
|
||||
const pixels = context.getImageData(0, 0, 192, 192).data;
|
||||
let visiblePixels = 0, bluePixels = 0, outerPixels = 0, compositedBluePixels = 0;
|
||||
let visiblePixels = 0, bluePixels = 0, outerPixels = 0, compositedBluePixels = 0, redSum = 0;
|
||||
let minimumBodyAlpha = 255;
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
redSum += pixels[i];
|
||||
const x = (i / 4) % 192 + 0.5 - 96;
|
||||
const y = Math.floor(i / 4 / 192) + 0.5 - 96;
|
||||
// Sample inside the feathered rim: map lines should not remain visible
|
||||
@@ -60,7 +61,7 @@ window.ready = new Promise(resolve => RuntimeLoader.getInstance(resolve)).then(r
|
||||
if ((pixels[i + 2] - pixels[i]) * pixels[i + 3] / 255 > 20) compositedBluePixels++;
|
||||
if (Math.hypot(x, y) > 79) outerPixels++;
|
||||
}
|
||||
snapshots.push({ image: canvas.toDataURL(), visiblePixels, bluePixels, outerPixels, compositedBluePixels, minimumBodyAlpha });
|
||||
snapshots.push({ image: canvas.toDataURL(), visiblePixels, bluePixels, outerPixels, compositedBluePixels, minimumBodyAlpha, redSum });
|
||||
}
|
||||
return snapshots;
|
||||
} finally {
|
||||
@@ -79,6 +80,7 @@ declare global {
|
||||
outerPixels: number;
|
||||
compositedBluePixels: number;
|
||||
minimumBodyAlpha: number;
|
||||
redSum: number;
|
||||
}[]>;
|
||||
}
|
||||
}
|
||||
@@ -194,6 +196,56 @@ test("original-size hover reflections follow the pointer instead of whitening th
|
||||
expect(result.recovered).toBe(true);
|
||||
});
|
||||
|
||||
test("the original reflection fades in over 150ms and fades out over 600ms without changing its material", async ({ page }, testInfo) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const steps = [
|
||||
{ state: "idle", frames: 90 },
|
||||
{ state: "hover", frames: 1, pointer: [64, 64] as [number, number] },
|
||||
{ state: "hover", frames: 4 },
|
||||
{ state: "hover", frames: 6 },
|
||||
{ state: "hover", frames: 180 },
|
||||
{ state: "idle", frames: 1 },
|
||||
{ state: "idle", frames: 11 },
|
||||
{ state: "idle", frames: 12 },
|
||||
{ state: "idle", frames: 24 }
|
||||
];
|
||||
const edited = await window.renderResource("modified", steps);
|
||||
const idle = await window.renderResource("modified", steps.map(({ frames }) => ({ state: "idle", frames })));
|
||||
const original = await window.renderResource("recolored", steps);
|
||||
return {
|
||||
brightness: edited.map((frame, index) => frame.redSum - idle[index].redSum),
|
||||
materialPreserved: edited[4].image === original[4].image,
|
||||
restored: edited[8].image === idle[8].image,
|
||||
images: edited.slice(1).map((frame) => frame.image)
|
||||
};
|
||||
});
|
||||
await page.evaluate((images) => {
|
||||
const labels = ["Enter 17ms", "Enter 83ms", "Enter 183ms", "Original reflection", "Exit 17ms", "Exit 200ms", "Exit 400ms", "Settled"];
|
||||
document.body.replaceChildren();
|
||||
document.body.style.cssText = "display:flex;flex-wrap:wrap;gap:16px;background:#f5f7fa";
|
||||
for (const [index, source] of images.entries()) {
|
||||
const figure = document.createElement("figure");
|
||||
figure.style.margin = "0";
|
||||
const image = new Image(192, 192);
|
||||
image.src = source;
|
||||
const caption = document.createElement("figcaption");
|
||||
caption.textContent = labels[index];
|
||||
figure.append(image, caption);
|
||||
document.body.append(figure);
|
||||
}
|
||||
}, result.images);
|
||||
const sequence = await page.screenshot({ path: "/tmp/nexttjwater-persona-timing.png" });
|
||||
await testInfo.attach("original-reflection-timing", { body: sequence, contentType: "image/png" });
|
||||
expect(result.brightness[1]).toBeLessThan(result.brightness[2]);
|
||||
expect(result.brightness[2]).toBeLessThan(result.brightness[3]);
|
||||
expect(result.brightness[3]).toBeGreaterThan(1000);
|
||||
expect(result.materialPreserved).toBe(true);
|
||||
expect(result.brightness[5]).toBeGreaterThan(result.brightness[6]);
|
||||
expect(result.brightness[6]).toBeGreaterThan(result.brightness[7]);
|
||||
expect(result.brightness[7]).toBeGreaterThan(0);
|
||||
expect(result.restored).toBe(true);
|
||||
});
|
||||
|
||||
test("the original disappears offline while the edited resource keeps moving and recovers", async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const steps = [
|
||||
|
||||
Reference in New Issue
Block a user