refactor: 登录页背景动画循环并补充调试入口#1221
Conversation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
… 60fps internal timing
…g scope Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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));| 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(); | ||
| } |
There was a problem hiding this comment.
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);
}There was a problem hiding this comment.
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.
| 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(); | ||
| }, |
There was a problem hiding this comment.
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).
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.
|
感谢 PR。 |
本地测试过没问题了。 |
变更说明
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.tsfrontend/src/features/auth/sign-in/components/animated-line-background.tsxfrontend/src/features/auth/sign-in/index.tsxfrontend/src/routeTree.gen.tsCloses #1217