interactive-background fixes
All checks were successful
publish.yml / publish (push) Successful in 1m3s

This commit is contained in:
2026-04-16 23:13:51 +02:00
parent 15294f95a1
commit 67676721d6

View File

@@ -290,7 +290,14 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy {
private reducedMotion = false; private reducedMotion = false;
private lowMotionMobile = false; private lowMotionMobile = false;
private mediaQuery!: MediaQueryList; private mediaQuery!: MediaQueryList;
private resizeFrameId: number | null = null;
private resizeSettleTimeout: ReturnType<typeof setTimeout> | null = null;
private lastCanvasWidth = 0;
private lastCanvasHeight = 0;
private lastCanvasDpr = 0;
private readonly pointerExcludeSelector = '[data-disable-bg-pointer]'; private readonly pointerExcludeSelector = '[data-disable-bg-pointer]';
private readonly resizeSettleDelayMs = 140;
private readonly resizeNoiseThresholdPx = 2;
ngAfterViewInit(): void { ngAfterViewInit(): void {
const canvas = this.canvasRef.nativeElement; const canvas = this.canvasRef.nativeElement;
@@ -306,14 +313,15 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy {
const mobileCount = Math.round(Math.min(54, Math.max(28, dynamicCount * 0.4))); const mobileCount = Math.round(Math.min(54, Math.max(28, dynamicCount * 0.4)));
const particleCount = this.reducedMotion ? 42 : (this.lowMotionMobile ? mobileCount : dynamicCount); const particleCount = this.reducedMotion ? 42 : (this.lowMotionMobile ? mobileCount : dynamicCount);
this.particleEngine = new ParticleEngine(context, particleCount); this.particleEngine = new ParticleEngine(context, particleCount);
this.resizeCanvas(); this.resizeCanvas(true);
this.renderInitialFrame(); this.renderInitialFrame();
if (!this.reducedMotion) { if (!this.reducedMotion) {
this.startAnimation(); this.startAnimation();
} }
} }
const onResize = () => this.resizeCanvas(); const visualViewport = window.visualViewport;
const onResize = () => this.scheduleCanvasResize();
const supportsPointerEvents = 'PointerEvent' in window; const supportsPointerEvents = 'PointerEvent' in window;
const onPointerMove = (event: PointerEvent) => this.handlePointerMove(event); const onPointerMove = (event: PointerEvent) => this.handlePointerMove(event);
const onPointerLeave = () => this.handlePointerLeave(); const onPointerLeave = () => this.handlePointerLeave();
@@ -333,7 +341,9 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy {
const onMotionChange = (event: MediaQueryListEvent) => this.handleMotionPreferenceChange(event.matches); const onMotionChange = (event: MediaQueryListEvent) => this.handleMotionPreferenceChange(event.matches);
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize, { passive: true });
visualViewport?.addEventListener('resize', onResize, { passive: true });
visualViewport?.addEventListener('scroll', onResize, { passive: true });
if (supportsPointerEvents) { if (supportsPointerEvents) {
window.addEventListener('pointermove', onPointerMove, { passive: true }); window.addEventListener('pointermove', onPointerMove, { passive: true });
@@ -356,6 +366,8 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy {
this.destroyCallbacks = [ this.destroyCallbacks = [
() => window.removeEventListener('resize', onResize), () => window.removeEventListener('resize', onResize),
() => visualViewport?.removeEventListener('resize', onResize),
() => visualViewport?.removeEventListener('scroll', onResize),
() => window.removeEventListener('pointermove', onPointerMove), () => window.removeEventListener('pointermove', onPointerMove),
() => window.removeEventListener('pointerleave', onPointerLeave), () => window.removeEventListener('pointerleave', onPointerLeave),
() => window.removeEventListener('pointerdown', onPointerDown), () => window.removeEventListener('pointerdown', onPointerDown),
@@ -375,22 +387,61 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.stopAnimation(); this.stopAnimation();
if (this.resizeFrameId !== null) {
cancelAnimationFrame(this.resizeFrameId);
this.resizeFrameId = null;
}
if (this.resizeSettleTimeout !== null) {
clearTimeout(this.resizeSettleTimeout);
this.resizeSettleTimeout = null;
}
for (const callback of this.destroyCallbacks) { for (const callback of this.destroyCallbacks) {
callback(); callback();
} }
this.destroyCallbacks = []; this.destroyCallbacks = [];
} }
private resizeCanvas(): void { private scheduleCanvasResize(): void {
if (this.resizeFrameId === null) {
this.resizeFrameId = requestAnimationFrame(() => {
this.resizeFrameId = null;
this.resizeCanvas(false);
});
}
if (this.resizeSettleTimeout !== null) {
clearTimeout(this.resizeSettleTimeout);
}
this.resizeSettleTimeout = setTimeout(() => {
this.resizeSettleTimeout = null;
this.resizeCanvas(true);
}, this.resizeSettleDelayMs);
}
private resizeCanvas(force: boolean): void {
if (!this.particleEngine) { if (!this.particleEngine) {
return; return;
} }
const canvas = this.canvasRef.nativeElement; const canvas = this.canvasRef.nativeElement;
const width = window.innerWidth; const viewport = this.getViewportSize();
const height = window.innerHeight; const width = Math.max(1, Math.round(viewport.width));
const height = Math.max(1, Math.round(viewport.height));
const dpr = Math.min(window.devicePixelRatio || 1, 2); const dpr = Math.min(window.devicePixelRatio || 1, 2);
const widthChanged = Math.abs(width - this.lastCanvasWidth) > this.resizeNoiseThresholdPx;
const heightChanged = Math.abs(height - this.lastCanvasHeight) > this.resizeNoiseThresholdPx;
const dprChanged = Math.abs(dpr - this.lastCanvasDpr) > 0.01;
if (!force && !widthChanged && !heightChanged && !dprChanged) {
return;
}
this.lastCanvasWidth = width;
this.lastCanvasHeight = height;
this.lastCanvasDpr = dpr;
canvas.width = Math.floor(width * dpr); canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr); canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
@@ -400,6 +451,21 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy {
this.renderInitialFrame(); this.renderInitialFrame();
} }
private getViewportSize(): { width: number; height: number } {
const visualViewport = window.visualViewport;
if (visualViewport) {
return {
width: visualViewport.width,
height: visualViewport.height,
};
}
return {
width: window.innerWidth,
height: window.innerHeight,
};
}
private startAnimation(): void { private startAnimation(): void {
if (this.frameId !== null) { if (this.frameId !== null) {
return; return;