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:
2026-10-28 11:46:14 +08:00
parent 4f0f34f460
commit e57e0f099f
12 changed files with 325 additions and 31 deletions
+28 -1
View File
@@ -12,6 +12,7 @@ const designUrl = new URL("../design/agent/obsidian-offline.json", import.meta.u
const defaultDesign = JSON.parse(await readFile(designUrl, "utf8"));
const materialUrl = new URL("../design/agent/obsidian-material.json", import.meta.url);
const defaultMaterial = JSON.parse(await readFile(materialUrl, "utf8"));
const hoverTiming = JSON.parse(await readFile(new URL("../src/shared/ai-elements/persona-hover-timing.json", import.meta.url), "utf8"));
function uint(value) {
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid Rive uint: ${value}`);
@@ -172,14 +173,40 @@ export function buildOfflinePersona(source, offline = defaultDesign, material =
const components = objects.slice(artboard, firstAnimation).filter((object) => object.type !== 447);
let animation;
let machineLayer;
for (const object of objects) {
if (object.type === 31) animation = valueOf(object, 55);
if (object.type === 31) {
animation = valueOf(object, 55);
if (animation === "hover_in" || animation === "hover_out") {
setValue(object, 57, Math.round((animation === "hover_in" ? hoverTiming.enterMs : hoverTiming.exitMs) * 60 / 1000));
}
}
// Change timing only, keeping the original reflection geometry and color.
if ((animation === "hover_in" || animation === "hover_out") && object.type === 30) {
const durationMs = animation === "hover_in" ? hoverTiming.enterMs : hoverTiming.exitMs;
setValue(object, 67, Math.round(valueOf(object, 67, 0) * durationMs / 1000));
setValue(object, 68, 2);
setValue(object, 69, 261);
}
if (object.type === 57) machineLayer = valueOf(object, 138);
if (machineLayer === "hover" && object.type === 65 && valueOf(object, 152, 0) === 12) {
setValue(object, 158, hoverTiming.blendMs);
}
// These tracks only control opacity and scale. Keep the complete sphere.
if ((animation === "asleep" || animation === "wake") && object.type === 30) {
setValue(object, 70, 1);
}
}
const hoverOutEnd = objects.findIndex((object) => object.type === 31 && valueOf(object, 55) === "on_loop_1");
// Keep the pointer constraint active until the outgoing reflection is fully
// transparent. The original hover_off timeline releases it afterwards.
objects.splice(hoverOutEnd, 0,
{ type: 25, values: [[51, 129]] },
{ type: 26, values: [[53, 172]] },
{ type: 30, values: [[68, 1], [70, 1]] }
);
const loops = [];
for (let number = 1; number <= 3; number++) {
const start = objects.findIndex((object) => object.type === 31 && valueOf(object, 55) === `on_loop_${number}`);