diff --git a/src/app/core-ui/main-header/play-button/play-button.component.ts b/src/app/core-ui/main-header/play-button/play-button.component.ts index 99a717ef44..6afc62e773 100644 --- a/src/app/core-ui/main-header/play-button/play-button.component.ts +++ b/src/app/core-ui/main-header/play-button/play-button.component.ts @@ -23,7 +23,8 @@ import { Task } from '../../../features/tasks/task.model'; import { WorkContext } from '../../../features/work-context/work-context.model'; import { TaskService } from '../../../features/tasks/task.service'; import { PomodoroService } from '../../../features/pomodoro/pomodoro.service'; -import { Subscription } from 'rxjs'; +import { animationFrameScheduler, Subscription } from 'rxjs'; +import { distinctUntilChanged, observeOn } from 'rxjs/operators'; @Component({ selector: 'play-button', @@ -333,19 +334,25 @@ export class PlayButtonComponent implements OnInit, OnDestroy { // Subscribe to task progress for circle animation this._subs.add( - this.taskService.currentTaskProgress$.subscribe((progressIN) => { - const circleSvgEl = this.circleSvg()?.nativeElement; - if (circleSvgEl) { - let progress = progressIN || 0; - if (progress > 1) { - progress = 1; + this.taskService.currentTaskProgress$ + .pipe( + // Align ring updates with the frame budget and skip duplicate ratios. + observeOn(animationFrameScheduler), + distinctUntilChanged(), + ) + .subscribe((progressIN) => { + const circleSvgEl = this.circleSvg()?.nativeElement; + if (circleSvgEl) { + let progress = progressIN || 0; + if (progress > 1) { + progress = 1; + } + // Calculate dashoffset: 0 when 0%, negative circumference when 100% + // This shows the completed portion of the circle + const dashOffset = this.circumference * -progress; + this._renderer.setStyle(circleSvgEl, 'stroke-dashoffset', dashOffset); } - // Calculate dashoffset: 0 when 0%, negative circumference when 100% - // This shows the completed portion of the circle - const dashOffset = this.circumference * -progress; - this._renderer.setStyle(circleSvgEl, 'stroke-dashoffset', dashOffset); - } - }), + }), ); } diff --git a/src/app/features/work-view/split/split.component.ts b/src/app/features/work-view/split/split.component.ts index 1a90388f3b..975e101402 100644 --- a/src/app/features/work-view/split/split.component.ts +++ b/src/app/features/work-view/split/split.component.ts @@ -10,8 +10,8 @@ import { Renderer2, viewChild, } from '@angular/core'; -import { fromEvent, Subscription } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { animationFrameScheduler, fromEvent, Subscription } from 'rxjs'; +import { observeOn, takeUntil } from 'rxjs/operators'; import { isTouchOnly } from '../../../util/is-touch-only'; import { MatFabButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; @@ -80,7 +80,11 @@ export class SplitComponent implements AfterViewInit { this.eventSubs = touchend$.subscribe(() => this.onMoveEnd()); const touchmove$ = fromEvent(document, 'touchmove') - .pipe(takeUntil(touchend$)) + .pipe( + takeUntil(touchend$), + // Run drag calculations at most once per frame to avoid layout thrash. + observeOn(animationFrameScheduler), + ) .subscribe((e: Event) => this.onMove(e as TouchEvent)); this.eventSubs.add(touchmove$); @@ -92,7 +96,11 @@ export class SplitComponent implements AfterViewInit { this.eventSubs = mouseup$.subscribe(() => this.onMoveEnd()); const mousemove$ = fromEvent(document, 'mousemove') - .pipe(takeUntil(mouseup$)) + .pipe( + takeUntil(mouseup$), + // Run drag calculations at most once per frame to avoid layout thrash. + observeOn(animationFrameScheduler), + ) .subscribe((e: Event) => this.onMove(e as MouseEvent)); this.eventSubs.add(mousemove$); diff --git a/src/app/features/work-view/work-view.component.ts b/src/app/features/work-view/work-view.component.ts index 8aedd4cea7..a11be669c3 100644 --- a/src/app/features/work-view/work-view.component.ts +++ b/src/app/features/work-view/work-view.component.ts @@ -19,6 +19,7 @@ import { LayoutService } from '../../core-ui/layout/layout.service'; import { TakeABreakService } from '../take-a-break/take-a-break.service'; import { ActivatedRoute } from '@angular/router'; import { + animationFrameScheduler, from, fromEvent, Observable, @@ -28,7 +29,7 @@ import { zip, } from 'rxjs'; import { TaskWithSubTasks } from '../tasks/task.model'; -import { delay, filter, map, switchMap } from 'rxjs/operators'; +import { delay, filter, map, observeOn, switchMap } from 'rxjs/operators'; import { fadeAnimation } from '../../ui/animations/fade.ani'; import { PlanningModeService } from '../planning-mode/planning-mode.service'; import { T } from '../../t.const'; @@ -156,7 +157,11 @@ export class WorkViewComponent implements OnInit, OnDestroy { filter((isChanging) => !isChanging), delay(50), switchMap(() => this.splitTopEl$), - switchMap((el) => fromEvent(el, 'scroll')), + switchMap((el) => + // Defer scroll reactions to the next frame so layoutService.isScrolled + // toggles happen in sync with the browser repaint. + fromEvent(el, 'scroll').pipe(observeOn(animationFrameScheduler)), + ), ); private _subs: Subscription = new Subscription();