Skip to content

Commit 1326d05

Browse files
committed
Refactor scroll-based components: implement debouncing for scroll event handlers in logo-scroll-rotate, scroll-rotate binder, and dfw-scroll-visibility components to improve performance. Enhance dfw-background-gears component with resizing transitions and update dfw-contact-map to utilize a new scroll positioning utility for better user experience.
1 parent b8996ed commit 1326d05

6 files changed

Lines changed: 91 additions & 103 deletions

File tree

src/ts/binders/scroll-rotate.binder.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Binder } from "@ribajs/core";
2+
import { debounceF } from "@ribajs/utils/src/control.js";
23

34
/**
45
* Rotates an element based on the window scroll position.
@@ -10,9 +11,9 @@ export class ScrollRotateBinder extends Binder<number, HTMLElement> {
1011

1112
private scrollHandler: (() => void) | null = null;
1213
private speed = 0.15;
13-
private animationFrameId: number | null = null;
1414

1515
private applyRotation() {
16+
if (!this.el.isConnected) return;
1617
const scrollY = window.scrollY || window.pageYOffset;
1718
const degrees = scrollY * this.speed;
1819
this.el.style.transform = `rotate(${degrees}deg)`;
@@ -26,15 +27,8 @@ export class ScrollRotateBinder extends Binder<number, HTMLElement> {
2627
}
2728

2829
bind(el: HTMLElement) {
29-
this.scrollHandler = () => {
30-
if (this.animationFrameId) {
31-
cancelAnimationFrame(this.animationFrameId);
32-
}
33-
this.animationFrameId = requestAnimationFrame(() => {
34-
this.applyRotation();
35-
this.animationFrameId = null;
36-
});
37-
};
30+
const debouncedApply = debounceF(() => this.applyRotation());
31+
this.scrollHandler = () => debouncedApply();
3832

3933
window.addEventListener('scroll', this.scrollHandler, { passive: true });
4034

@@ -47,9 +41,5 @@ export class ScrollRotateBinder extends Binder<number, HTMLElement> {
4741
window.removeEventListener('scroll', this.scrollHandler);
4842
this.scrollHandler = null;
4943
}
50-
if (this.animationFrameId) {
51-
cancelAnimationFrame(this.animationFrameId);
52-
this.animationFrameId = null;
53-
}
5444
}
5545
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
dfw-background-gears {
22
display: block;
3+
transition: opacity 0.15s ease-out;
4+
5+
&.dfw-background-gears--resizing {
6+
opacity: 0;
7+
pointer-events: none;
8+
}
39
}

src/ts/components/dfw-background-gears/dfw-background-gears.component.ts

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component } from "@ribajs/core";
22
import { EventDispatcher } from "@ribajs/events";
3+
import { debounceCb, debounceF } from "@ribajs/utils/src/control.js";
34
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
45

56
import templateHtml from "./dfw-background-gears.component.html?raw";
@@ -34,13 +35,16 @@ export class DfwBackgroundGearsComponent extends Component {
3435

3536
protected autobind = true;
3637

37-
private shineRafId: number | null = null;
3838
private resizeObserver: ResizeObserver | null = null;
39+
private static readonly RESIZE_DEBOUNCE_MS = 450;
3940
private routerUnsubscribe: (() => void) | null = null;
40-
private readonly boundUpdateShineFromScroll = () => this.updateShineFromScroll();
41+
private debouncedShineFromScroll!: () => void;
42+
private debouncedShineFromPointer!: (e: MouseEvent) => void;
43+
private readonly boundUpdateShineFromScroll = () => this.debouncedShineFromScroll();
4144
private readonly boundUpdateShineFromPointer = (e: MouseEvent) =>
42-
this.updateShineFromPointer(e);
43-
private readonly boundUpdateHeightToPage = () => this.updateHeightToPage();
45+
this.debouncedShineFromPointer(e);
46+
private readonly boundOnResize = () => this.onResize();
47+
private debouncedResizeDone!: () => void;
4448
private readonly boundOnTransitionCompleted = () => this.onTransitionCompleted();
4549
private readonly boundOnInitStateChange = (
4650
_viewId: string,
@@ -59,15 +63,30 @@ export class DfwBackgroundGearsComponent extends Component {
5963
{ src: gear2Url, speed: 0.32, class: "bg-gear--5", maskStyle: { "--gear-mask": `url("${gear2Url}")` } },
6064
] as const;
6165

62-
/** Set gears container height to full page height so gear positions (top: X%) distribute along the page. */
66+
/** Hide gears and schedule debounced repositioning when viewport size changes. */
67+
private onResize(): void {
68+
(this as unknown as HTMLElement).classList.add("dfw-background-gears--resizing");
69+
this.debouncedResizeDone();
70+
}
71+
72+
/** Set gears container height to content height so gear positions (top: X%) distribute along the page.
73+
* Height is read while gears container is collapsed to avoid the gears contributing to document height. */
6374
private updateHeightToPage(): void {
75+
const container = this.parentElement as HTMLElement | null;
76+
const self = this as unknown as HTMLElement;
77+
78+
if (container?.classList.contains("background-gears")) {
79+
container.style.height = "0";
80+
}
81+
self.style.height = "0";
82+
6483
const h = document.documentElement.scrollHeight;
6584
const px = `${h}px`;
66-
const container = this.parentElement;
85+
6786
if (container?.classList.contains("background-gears")) {
68-
(container as HTMLElement).style.height = px;
87+
container.style.height = px;
6988
}
70-
(this as unknown as HTMLElement).style.height = px;
89+
self.style.height = px;
7190
}
7291

7392
/** Collapse gears container and remove gear nodes so document height is not preserved on page change. */
@@ -130,39 +149,44 @@ export class DfwBackgroundGearsComponent extends Component {
130149
document.documentElement.style.setProperty("--shine-y", y);
131150
}
132151

133-
private updateShineFromScroll(): void {
134-
if (this.shineRafId !== null) return;
135-
this.shineRafId = requestAnimationFrame(() => {
136-
this.shineRafId = null;
137-
const scrollY = window.scrollY ?? document.documentElement.scrollTop;
138-
const innerHeight = window.innerHeight;
139-
const scrollHeight = document.documentElement.scrollHeight;
140-
const maxScroll = Math.max(0, scrollHeight - innerHeight);
141-
const progress = maxScroll > 0 ? scrollY / maxScroll : 0;
142-
const cycles = 3;
143-
const phase = (progress * cycles) % 1;
144-
const yPercent =
145-
phase <= 0.5 ? phase * 200 : (1 - phase) * 200;
146-
this.setShine(this.shineX, `${Math.min(100, Math.max(0, yPercent))}%`);
147-
});
152+
private doUpdateShineFromScroll(): void {
153+
if (!this.isConnected) return;
154+
const scrollY = window.scrollY ?? document.documentElement.scrollTop;
155+
const innerHeight = window.innerHeight;
156+
const scrollHeight = document.documentElement.scrollHeight;
157+
const maxScroll = Math.max(0, scrollHeight - innerHeight);
158+
const progress = maxScroll > 0 ? scrollY / maxScroll : 0;
159+
const cycles = 3;
160+
const phase = (progress * cycles) % 1;
161+
const yPercent =
162+
phase <= 0.5 ? phase * 200 : (1 - phase) * 200;
163+
this.setShine(this.shineX, `${Math.min(100, Math.max(0, yPercent))}%`);
148164
}
149165

150-
private updateShineFromPointer(e: MouseEvent): void {
151-
if (this.shineRafId !== null) return;
152-
this.shineRafId = requestAnimationFrame(() => {
153-
this.shineRafId = null;
154-
const xPercent = (e.clientX / window.innerWidth) * 100;
155-
this.setShine(`${xPercent}%`, this.shineY);
156-
});
166+
private doUpdateShineFromPointer(e: MouseEvent): void {
167+
if (!this.isConnected) return;
168+
const xPercent = (e.clientX / window.innerWidth) * 100;
169+
this.setShine(`${xPercent}%`, this.shineY);
157170
}
158171

159172
protected connectedCallback() {
160173
super.connectedCallback();
161174
this.init(DfwBackgroundGearsComponent.observedAttributes);
162175

176+
this.debouncedResizeDone = debounceCb(() => {
177+
if (!this.isConnected) return;
178+
(this as unknown as HTMLElement).classList.remove("dfw-background-gears--resizing");
179+
this.updateHeightToPage();
180+
this.scope.backgroundGears = this.buildGearsWithRandomLayout();
181+
this.view?.update(this.scope);
182+
}, DfwBackgroundGearsComponent.RESIZE_DEBOUNCE_MS);
183+
184+
this.debouncedShineFromScroll = debounceF(() => this.doUpdateShineFromScroll());
185+
this.debouncedShineFromPointer = debounceF((e: MouseEvent) => this.doUpdateShineFromPointer(e));
186+
163187
this.scope.backgroundGears = this.buildGearsWithRandomLayout();
164188
this.updateHeightToPage();
165-
this.updateShineFromScroll();
189+
this.debouncedShineFromScroll();
166190

167191
const dispatcher = EventDispatcher.getInstance(ROUTER_VIEW_ID);
168192
dispatcher.on("initStateChange", this.boundOnInitStateChange);
@@ -176,8 +200,8 @@ export class DfwBackgroundGearsComponent extends Component {
176200
passive: true,
177201
});
178202
window.addEventListener("resize", this.boundUpdateShineFromScroll);
179-
window.addEventListener("resize", this.boundUpdateHeightToPage);
180-
this.resizeObserver = new ResizeObserver(() => this.updateHeightToPage());
203+
window.addEventListener("resize", this.boundOnResize);
204+
this.resizeObserver = new ResizeObserver(this.boundOnResize);
181205
this.resizeObserver.observe(document.body);
182206
if (window.matchMedia("(pointer: fine)").matches) {
183207
window.addEventListener("mousemove", this.boundUpdateShineFromPointer);
@@ -187,15 +211,11 @@ export class DfwBackgroundGearsComponent extends Component {
187211
protected disconnectedCallback(): void {
188212
this.routerUnsubscribe?.();
189213
this.routerUnsubscribe = null;
190-
if (this.shineRafId !== null) {
191-
cancelAnimationFrame(this.shineRafId);
192-
this.shineRafId = null;
193-
}
194214
this.resizeObserver?.disconnect();
195215
this.resizeObserver = null;
196216
window.removeEventListener("scroll", this.boundUpdateShineFromScroll);
197217
window.removeEventListener("resize", this.boundUpdateShineFromScroll);
198-
window.removeEventListener("resize", this.boundUpdateHeightToPage);
218+
window.removeEventListener("resize", this.boundOnResize);
199219
window.removeEventListener("mousemove", this.boundUpdateShineFromPointer);
200220
super.disconnectedCallback();
201221
}

src/ts/components/dfw-contact-map/dfw-contact-map.component.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
import { Component } from "@ribajs/core";
2-
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
3-
4-
function debounce<T extends (...args: unknown[]) => void>(
5-
fn: T,
6-
ms: number
7-
): (...args: Parameters<T>) => void {
8-
let timeoutId: ReturnType<typeof setTimeout>;
9-
return (...args: Parameters<T>) => {
10-
clearTimeout(timeoutId);
11-
timeoutId = setTimeout(() => fn(...args), ms);
12-
};
13-
}
14-
15-
/**
16-
* Scrolls the map wrapper so the image is centered (like mm-map in markus-morische-rechtsanwalt-website).
17-
*/
18-
function scrollToCenter(wrapper: HTMLElement): void {
19-
wrapper.scrollLeft = (wrapper.scrollWidth - wrapper.clientWidth) / 2;
20-
wrapper.scrollTop = (wrapper.scrollHeight - wrapper.clientHeight) / 2;
21-
}
2+
import { debounceCb } from "@ribajs/utils/src/control.js";
3+
import { hasChildNodesTrim, scrollToPosition } from "@ribajs/utils/src/dom.js";
224

235
export interface DfwContactMapScope {
246
scrollWrapperEl: HTMLDivElement | null;
@@ -39,7 +21,7 @@ export class DfwContactMapComponent extends Component {
3921
center: this.center.bind(this),
4022
};
4123

42-
private boundCenter = debounce(this.center.bind(this), 100);
24+
private boundCenter = debounceCb(this.center.bind(this), 100);
4325

4426
protected connectedCallback() {
4527
super.connectedCallback();
@@ -64,7 +46,7 @@ export class DfwContactMapComponent extends Component {
6446
public center(): void {
6547
const wrapper = this.scope.scrollWrapperEl;
6648
if (wrapper) {
67-
scrollToCenter(wrapper);
49+
scrollToPosition(wrapper, "center", "both", "auto");
6850
}
6951
}
7052

src/ts/components/dfw-scroll-visibility/dfw-scroll-visibility.component.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Component } from "@ribajs/core";
2+
import { debounceF } from "@ribajs/utils/src/control.js";
23
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
34

45
const VISIBLE_SCROLL_START = 0.1; // show after 10% from top
@@ -19,42 +20,36 @@ export class DfwScrollVisibilityComponent extends Component {
1920
return [];
2021
}
2122

22-
private rafId: number | null = null;
23-
private readonly boundUpdateVisibility = () => this.updateVisibility();
23+
private debouncedUpdateVisibility!: () => void;
24+
private readonly boundUpdateVisibility = () => this.debouncedUpdateVisibility();
2425

2526
protected connectedCallback() {
2627
super.connectedCallback();
2728
this.setAttribute("data-visible", "false");
2829
this.init(DfwScrollVisibilityComponent.observedAttributes);
30+
31+
this.debouncedUpdateVisibility = debounceF(() => {
32+
if (!this.isConnected) return;
33+
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
34+
const visible =
35+
maxScroll <= 0 ||
36+
(window.scrollY >= maxScroll * VISIBLE_SCROLL_START &&
37+
window.scrollY <= maxScroll * VISIBLE_SCROLL_END);
38+
this.setAttribute("data-visible", visible ? "true" : "false");
39+
});
2940
}
3041

3142
protected async afterBind() {
3243
await super.afterBind();
33-
this.updateVisibility();
44+
this.debouncedUpdateVisibility();
3445
window.addEventListener("scroll", this.boundUpdateVisibility, { passive: true });
3546
}
3647

3748
protected disconnectedCallback() {
3849
window.removeEventListener("scroll", this.boundUpdateVisibility);
39-
if (this.rafId !== null) {
40-
cancelAnimationFrame(this.rafId);
41-
}
4250
super.disconnectedCallback();
4351
}
4452

45-
private updateVisibility(): void {
46-
if (this.rafId !== null) return;
47-
this.rafId = requestAnimationFrame(() => {
48-
this.rafId = null;
49-
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
50-
const visible =
51-
maxScroll <= 0 ||
52-
(window.scrollY >= maxScroll * VISIBLE_SCROLL_START &&
53-
window.scrollY <= maxScroll * VISIBLE_SCROLL_END);
54-
this.setAttribute("data-visible", visible ? "true" : "false");
55-
});
56-
}
57-
5853
protected requiredAttributes(): string[] {
5954
return [];
6055
}

src/ts/logo-scroll-rotate.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { debounceF } from "@ribajs/utils/src/control.js";
2+
13
/**
24
* Applies scroll-based rotation to static elements with data-scroll-rotate (e.g. .logo-gear).
35
* Same behavior as ScrollRotateBinder; used for static Pug-rendered logo so no Riba component is needed.
@@ -18,16 +20,9 @@ function updateAll() {
1820
});
1921
}
2022

21-
let rafId: number | null = null;
22-
function onScroll() {
23-
if (rafId !== null) return;
24-
rafId = requestAnimationFrame(() => {
25-
rafId = null;
26-
updateAll();
27-
});
28-
}
29-
3023
export function initLogoScrollRotate(): void {
24+
const onScroll = debounceF(updateAll);
25+
3126
updateAll();
32-
window.addEventListener("scroll", onScroll, { passive: true });
27+
window.addEventListener("scroll", () => onScroll(), { passive: true });
3328
}

0 commit comments

Comments
 (0)