fix(island): 开合不再震动——去掉弹簧过冲 + 锚点整数化
问题(用户反馈"灵动岛开合的时候不要震动"): 之前虽然有「收回不抖动」断言,但它只看窗口宽度单调收缩,漏掉了两处真实回弹。 逐帧采样卡片真实屏幕矩形后复现: - 展开时窗口冲到 411x329(目标 404x316)→ 主进程用了 easeOutBack 弹簧缓动,尺寸超过目标再弹回 - 卡片被放大到 400.6x311.6(目标 396x310)→ 卡片入场用 cubic-bezier(0.34,1.56,0.64,1) 缩放,先超过 1 再回落 - 卡片中心左右波动 1.00px、上边缘波动 2.88px → 逐帧 Math.round 在 0.5 边界跳变 + transform 位移 修复: 1. 缓动单调化: 删除 easeOutBack,展开/收回统一 easeOutCubic(窗口与卡片都不再过冲) 2. 锚点整数化且与尺寸解耦: 新增 resolveAnchor(),停靠位置决定不动的那条边/中心,动画前算一次 3. 卡片宽度取偶数 + boundsFor 统一推导窗口矩形(消除逐帧 0.5px 抖动) 4. 卡片入场去掉 scale/translateY,只做 180ms 淡入,形变完全交给窗口尺寸 5. 顺带修掉新探针暴露的真实缺陷: 隐藏后再弹出胶囊时透明度被硬编码为 1,忽略了个性化设置 验证(verify:desktop 49/49,修复前后对比): - 展开: 卡片中心波动 0.00px / 上边缘 0.00px,最大窗口 404x316(=目标)、最大卡片 396x308,单调=true - 收回: 0.00px / 0.00px,最小窗口 268,宽度序列单调无回跳 - 透明度 0.62 生效;四个停靠位置、置顶、动画开关/速度、持久化全部通过 - verify:e2e 133/133、verify:web 19/19 不受影响 文档: README 形变动画章节更新为「单调不过冲 + 整数锚点」,并记录修复前后实测数据
This commit is contained in:
1 parent
97a89de944
commit
9c9ac69ac6
4 files changed
+295
-81
No files matched your search
@@ -654,6 +654,123 @@ async function runIslandChecks(
|
||||
`(期望 ${currentAppearance.width}x${currentAppearance.height})`,
|
||||
);
|
||||
|
||||
// 10.5) 「开合不震动」实测:把窗口矩形与卡片的**真实屏幕矩形**逐帧采样,
|
||||
// 断言 ①卡片的水平中心与上边缘绝对不动(锚点稳定,不左右/上下跳动)
|
||||
// ②卡片与窗口的尺寸单调变化且**从不过冲**(不再有"弹一下"的回弹)
|
||||
const motionSamples: {
|
||||
winW: number;
|
||||
winH: number;
|
||||
cardCx: number;
|
||||
cardTop: number;
|
||||
cardW: number;
|
||||
cardH: number;
|
||||
}[] = [];
|
||||
|
||||
const sampleCardRect = async (): Promise<void> => {
|
||||
const bounds = islandWindow?.getBounds();
|
||||
const rect = await islandWindow?.webContents
|
||||
.executeJavaScript(
|
||||
`(() => {
|
||||
const card = document.querySelector('.island-card');
|
||||
if (!card) return null;
|
||||
const r = card.getBoundingClientRect();
|
||||
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
||||
})()`,
|
||||
)
|
||||
.catch(() => null);
|
||||
if (!bounds || !rect) return;
|
||||
motionSamples.push({
|
||||
winW: bounds.width,
|
||||
winH: bounds.height,
|
||||
cardCx: bounds.x + rect.left + rect.width / 2,
|
||||
cardTop: bounds.y + rect.top,
|
||||
cardW: rect.width,
|
||||
cardH: rect.height,
|
||||
});
|
||||
};
|
||||
|
||||
const runMotionProbe = async (action: 'expand' | 'collapse'): Promise<void> => {
|
||||
motionSamples.length = 0;
|
||||
island.handleAction({ action });
|
||||
const deadline = Date.now() + 900;
|
||||
while (Date.now() < deadline) {
|
||||
await sampleCardRect();
|
||||
await sleep(12);
|
||||
}
|
||||
};
|
||||
|
||||
island.handleAction({ action: 'dismiss' });
|
||||
await sleep(250);
|
||||
island.pushNotification(makeNotification('smoke-motion', 'NORMAL', '开合不震动校验'), { inClass: false });
|
||||
await sleep(350);
|
||||
|
||||
const expandedCard = { width: ISLAND_SIZES.expanded.width - 8, height: ISLAND_SIZES.expanded.height - 6 };
|
||||
const pillCard = { width: ISLAND_SIZES.pill.width - 8, height: ISLAND_SIZES.pill.height - 6 };
|
||||
|
||||
await runMotionProbe('expand');
|
||||
const expandSamples = [...motionSamples];
|
||||
const cxSpread = expandSamples.length
|
||||
? Math.max(...expandSamples.map((item) => item.cardCx)) -
|
||||
Math.min(...expandSamples.map((item) => item.cardCx))
|
||||
: 99;
|
||||
const topSpreadExpand = expandSamples.length
|
||||
? Math.max(...expandSamples.map((item) => item.cardTop)) -
|
||||
Math.min(...expandSamples.map((item) => item.cardTop))
|
||||
: 99;
|
||||
const maxCardW = expandSamples.length ? Math.max(...expandSamples.map((item) => item.cardW)) : 0;
|
||||
const maxCardH = expandSamples.length ? Math.max(...expandSamples.map((item) => item.cardH)) : 0;
|
||||
const maxWinW = expandSamples.length ? Math.max(...expandSamples.map((item) => item.winW)) : 0;
|
||||
const maxWinH = expandSamples.length ? Math.max(...expandSamples.map((item) => item.winH)) : 0;
|
||||
const expandMonotonic = expandSamples.every(
|
||||
(item, index) => index === 0 || item.winW >= (expandSamples[index - 1]?.winW ?? 0) - 1,
|
||||
);
|
||||
const expandNoOvershoot =
|
||||
maxCardW <= expandedCard.width + 0.5 &&
|
||||
maxCardH <= expandedCard.height + 0.5 &&
|
||||
maxWinW <= ISLAND_SIZES.expanded.width + 1 &&
|
||||
maxWinH <= ISLAND_SIZES.expanded.height + 1;
|
||||
record(
|
||||
'展开过程不震动(锚点不动 + 尺寸不过冲)',
|
||||
expandSamples.length >= 5 &&
|
||||
cxSpread <= 0.5 &&
|
||||
topSpreadExpand <= 0.5 &&
|
||||
expandMonotonic &&
|
||||
expandNoOvershoot,
|
||||
`采样=${expandSamples.length} 卡片中心波动=${cxSpread.toFixed(2)}px 上边缘波动=${topSpreadExpand.toFixed(2)}px ` +
|
||||
`最大卡片=${maxCardW.toFixed(1)}x${maxCardH.toFixed(1)}(目标 ${expandedCard.width}x${expandedCard.height})` +
|
||||
` 最大窗口=${maxWinW}x${maxWinH}(目标 ${ISLAND_SIZES.expanded.width}x${ISLAND_SIZES.expanded.height}) 单调=${expandMonotonic}`,
|
||||
);
|
||||
|
||||
await runMotionProbe('collapse');
|
||||
const collapseSamples = [...motionSamples];
|
||||
const cxSpreadCollapse = collapseSamples.length
|
||||
? Math.max(...collapseSamples.map((item) => item.cardCx)) -
|
||||
Math.min(...collapseSamples.map((item) => item.cardCx))
|
||||
: 99;
|
||||
const topSpreadCollapse = collapseSamples.length
|
||||
? Math.max(...collapseSamples.map((item) => item.cardTop)) -
|
||||
Math.min(...collapseSamples.map((item) => item.cardTop))
|
||||
: 99;
|
||||
const minWinW = collapseSamples.length ? Math.min(...collapseSamples.map((item) => item.winW)) : 0;
|
||||
const minCardW = collapseSamples.length ? Math.min(...collapseSamples.map((item) => item.cardW)) : 0;
|
||||
const collapseMonotonic = collapseSamples.every(
|
||||
(item, index) =>
|
||||
index === 0 || item.winW <= (collapseSamples[index - 1]?.winW ?? Number.MAX_SAFE_INTEGER) + 1,
|
||||
);
|
||||
const collapseNoOvershoot = minCardW >= pillCard.width - 0.5 && minWinW >= ISLAND_SIZES.pill.width - 1;
|
||||
record(
|
||||
'收回过程不震动(锚点不动 + 尺寸不过冲)',
|
||||
collapseSamples.length >= 5 &&
|
||||
cxSpreadCollapse <= 0.5 &&
|
||||
topSpreadCollapse <= 0.5 &&
|
||||
collapseMonotonic &&
|
||||
collapseNoOvershoot,
|
||||
`采样=${collapseSamples.length} 卡片中心波动=${cxSpreadCollapse.toFixed(2)}px 上边缘波动=${topSpreadCollapse.toFixed(2)}px ` +
|
||||
`最小卡片宽=${minCardW.toFixed(1)}(目标 ${pillCard.width}) 最小窗口宽=${minWinW}(目标 ${ISLAND_SIZES.pill.width}) 单调=${collapseMonotonic}`,
|
||||
);
|
||||
|
||||
// 收尾:保持"胶囊可见"状态——后续个性化用例(透明度 / 位置)需要窗口可见才会立即生效
|
||||
await sleep(250);
|
||||
// 11) 个性化设置的其余参数:透明度 / 主题色 / 动画开关 / 速度 / 位置 / 置顶 / 持久化
|
||||
// (需求 2 的完整清单,逐项断言真实窗口属性,而不是只看设置页显示)
|
||||
const workArea = screen.getPrimaryDisplay().workArea;
|
||||
|
||||
Reference in new issue
Block a user