Skip to content

refactor: 登录页背景动画循环并补充调试入口#1221

Merged
looplj merged 5 commits intolooplj:release/v0.9.xfrom
llc1123:fix/1217-login-animation-perf
Mar 30, 2026
Merged

refactor: 登录页背景动画循环并补充调试入口#1221
looplj merged 5 commits intolooplj:release/v0.9.xfrom
llc1123:fix/1217-login-animation-perf

Conversation

@llc1123
Copy link
Copy Markdown
Contributor

@llc1123 llc1123 commented Mar 30, 2026

变更说明

  • 将登录页粒子背景的粒子初始化、位置更新、连线绘制和表单避让逻辑拆分到独立的 animated-line-background.engine.ts,组件只保留画布生命周期和调度逻辑
  • 将动画循环改为基于固定步长的帧调度:增加 targetFps、累计器、最大补帧时长和单帧步数限制,并在页面隐藏/恢复时停止或重启动画,避免刷新率和长时间切后台直接影响动画推进
  • 仅在开发环境且带 __axonhub_debug_animation=1 查询参数时,向 window.__AXONHUB_SIGNIN_ANIMATION__ 暴露重置、快照和模拟方法,便于排查动画帧推进与粒子状态
  • 为登录页动画层和画布补充 data-testid,并在登录页外层增加单独的动画容器节点,方便测试与定位
  • 同步更新 frontend/src/routeTree.gen.ts 生成结果

影响文件

  • frontend/src/features/auth/sign-in/components/animated-line-background.engine.ts
  • frontend/src/features/auth/sign-in/components/animated-line-background.tsx
  • frontend/src/features/auth/sign-in/index.tsx
  • frontend/src/routeTree.gen.ts

Closes #1217

Copilot AI review requested due to automatic review settings March 30, 2026 05:23
@llc1123 llc1123 changed the title perf(login): reduce sign-in animation rendering cost 重构登录页背景动画循环并补充调试入口 Mar 30, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the AnimatedLineBackground component by moving its core animation logic into a dedicated engine file and implementing a fixed time-step animation loop for more consistent physics. It also adds a diagnostic mode for debugging and improves lifecycle management, such as pausing the animation when the page is hidden. The review feedback suggests simplifying the particle placement logic to avoid complex fallbacks and optimizing the rendering pass by using Path2D to reduce iterations over the particle array.

Comment on lines +125 to +144
let x = 0;
let y = 0;
let attempts = 0;

do {
x = canvasWidth / 2 + 20 + Math.random() * (canvasWidth / 2 - 20);
y = Math.random() * canvasHeight;
attempts++;
} while (isInFormArea(x, y, bounds) && attempts < 30);

if (isInFormArea(x, y, bounds)) {
if (Math.random() > 0.5) {
x =
Math.random() > 0.5
? canvasWidth / 2 + 20 + Math.random() * (bounds.formLeft - canvasWidth / 2 - 20)
: bounds.formRight + Math.random() * (canvasWidth - bounds.formRight - 20);
} else {
y = Math.random() > 0.5 ? Math.random() * bounds.formTop : bounds.formBottom + Math.random() * (canvasHeight - bounds.formBottom);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for placing particles on the right side of the screen while avoiding the form area is quite complex, especially the fallback mechanism after 30 attempts. This can be simplified for better readability and maintainability.

Instead of a limited number of attempts and a complex fallback, you can use a do-while loop that continues until a valid position is found. Given that the form doesn't cover the entire area, the risk of an infinite loop is negligible, and the code becomes much cleaner.

    let x: number;
    let y: number;

    do {
      x = canvasWidth / 2 + 20 + Math.random() * (canvasWidth / 2 - 20);
      y = Math.random() * canvasHeight;
    } while (isInFormArea(x, y, bounds));

Comment on lines +213 to +239
let hasLeftParticles = false;
ctx.beginPath();
for (let index = 0; index < particles.length; index += 1) {
const dot = particles[index];
if (!isInFormArea(dot.x, dot.y, bounds) && dot.x < canvasWidth / 2) {
ctx.rect(dot.x - 1.5, dot.y - 1.5, 3, 3);
hasLeftParticles = true;
}
}
if (hasLeftParticles) {
ctx.fillStyle = LEFT_PARTICLE_FILL_STYLE;
ctx.fill();
}

let hasRightParticles = false;
ctx.beginPath();
for (let index = 0; index < particles.length; index += 1) {
const dot = particles[index];
if (!isInFormArea(dot.x, dot.y, bounds) && dot.x >= canvasWidth / 2) {
ctx.rect(dot.x - 1.5, dot.y - 1.5, 3, 3);
hasRightParticles = true;
}
}
if (hasRightParticles) {
ctx.fillStyle = RIGHT_PARTICLE_FILL_STYLE;
ctx.fill();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve performance, you can avoid iterating over the particles array twice when rendering. You can do this in a single pass by using Path2D objects to build the paths for left and right particles separately, and then drawing them. This is more efficient, especially as the number of particles grows.

  const leftPath = new Path2D();
  const rightPath = new Path2D();
  let hasLeftParticles = false;
  let hasRightParticles = false;

  for (const dot of particles) {
    if (!isInFormArea(dot.x, dot.y, bounds)) {
      if (dot.x < canvasWidth / 2) {
        leftPath.rect(dot.x - 1.5, dot.y - 1.5, 3, 3);
        hasLeftParticles = true;
      } else {
        rightPath.rect(dot.x - 1.5, dot.y - 1.5, 3, 3);
        hasRightParticles = true;
      }
    }
  }

  if (hasLeftParticles) {
    ctx.fillStyle = LEFT_PARTICLE_FILL_STYLE;
    ctx.fill(leftPath);
  }

  if (hasRightParticles) {
    ctx.fillStyle = RIGHT_PARTICLE_FILL_STYLE;
    ctx.fill(rightPath);
  }

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets the high GPU usage on the sign-in page by refactoring the animated background into a dedicated “engine” module and regenerating the TanStack Router route tree after auth page updates.

Changes:

  • Refactors the sign-in animated background to use a fixed-timestep update loop and shared engine helpers.
  • Adds a lightweight wrapper/test hooks around the sign-in animation layer.
  • Regenerates routeTree.gen.ts (notably updating trailing-slash full paths for several routes).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
frontend/src/routeTree.gen.ts Regenerated route tree/types after auth-related updates (full paths now include trailing slashes for several index routes).
frontend/src/features/auth/sign-in/index.tsx Wraps the animation in a testable container element.
frontend/src/features/auth/sign-in/components/animated-line-background.tsx Moves simulation/render logic to a fixed-timestep loop and adds optional dev diagnostics exposure.
frontend/src/features/auth/sign-in/components/animated-line-background.engine.ts New engine module encapsulating particle init/update/render functions and animation config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +142 to +164
const processAnimationFrame = useCallback(
(deltaMs: number) => {
const safeDeltaMs = Number.isFinite(deltaMs) ? Math.max(0, deltaMs) : 0;
const clampedDeltaMs = Math.min(safeDeltaMs, animationConfig.maxCatchUpMs);

lastFrameDeltaMsRef.current = safeDeltaMs;
lastClampedDeltaMsRef.current = clampedDeltaMs;
accumulatorRef.current += clampedDeltaMs;

let steps = 0;
while (accumulatorRef.current >= animationConfig.frameIntervalMs && steps < animationConfig.maxStepsPerFrame) {
accumulatorRef.current -= animationConfig.frameIntervalMs;
applyAnimationStep(animationConfig.frameIntervalMs);
steps += 1;
}

const xa = (Math.random() * 1 - 0.5) * 0.5;
const ya = (Math.random() * 1 - 0.5) * 0.5;
lastFrameStepCountRef.current = steps;
if (steps === 0) {
lastAppliedDeltaMsRef.current = 0;
}

particlesRef.current.push({
x,
y,
xa,
ya,
max: 5000,
});
}
}, [canvasRef]);
renderFrame();
},
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processAnimationFrame always calls renderFrame(), so the canvas still re-renders on every requestAnimationFrame tick (e.g., 240Hz displays will still render ~240 times/sec). This keeps GPU usage tied to refresh rate and likely undermines the stated goal of capping work to animationConfig.targetFps. Consider throttling rendering to the fixed timestep as well (e.g., render only when at least one simulation step ran, or maintain a separate render accumulator / dirty flag for mouse movement).

Copilot uses AI. Check for mistakes.
Throttle sign-in canvas redraws to actual simulation, pointer, and bounds changes so high-refresh displays no longer drive unnecessary rendering. Measure the auth card from the DOM so the animation exclusion zone stays aligned across responsive auth layouts.
@looplj
Copy link
Copy Markdown
Owner

looplj commented Mar 30, 2026

感谢 PR。
我确认下,本地测试过,可以合并了吗。

@llc1123
Copy link
Copy Markdown
Contributor Author

llc1123 commented Mar 30, 2026

感谢 PR。 我确认下,本地测试过,可以合并了吗。

本地测试过没问题了。

@looplj looplj changed the title 重构登录页背景动画循环并补充调试入口 refactor: 登录页背景动画循环并补充调试入口 Mar 30, 2026
@looplj looplj merged commit 3b23a3d into looplj:release/v0.9.x Mar 30, 2026
2 checks passed
@llc1123 llc1123 deleted the fix/1217-login-animation-perf branch March 30, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug/错误]: 登录界面左侧动画大量占用 GPU

3 participants