From e06a5a29b4ebac37e1c957cd284eee03e410be8a Mon Sep 17 00:00:00 2001 From: tikaiz Date: Wed, 8 Apr 2026 09:27:45 +0200 Subject: [PATCH] Add interactivity --- .../interactive-background.ts | 167 ++++++++++++++++-- 1 file changed, 151 insertions(+), 16 deletions(-) diff --git a/src/components/interactive-background/interactive-background.ts b/src/components/interactive-background/interactive-background.ts index 1955efd..3f5c14b 100644 --- a/src/components/interactive-background/interactive-background.ts +++ b/src/components/interactive-background/interactive-background.ts @@ -180,6 +180,7 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy { private pointerX = 0; private pointerY = 0; private pointerActive = false; + private activeTouchId: number | null = null; private reducedMotion = false; private mediaQuery!: MediaQueryList; @@ -203,25 +204,61 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy { this.isCursorVisible = this.getMediaQuery('(pointer: fine)').matches; const onResize = () => this.resizeCanvas(); - const onMove = (event: PointerEvent) => this.handlePointerMove(event); - const onLeave = () => this.handlePointerLeave(); - const onDown = () => (this.isPointerDown = true); - const onUp = () => (this.isPointerDown = false); + const supportsPointerEvents = 'PointerEvent' in window; + const onPointerMove = (event: PointerEvent) => this.handlePointerMove(event); + const onPointerLeave = () => this.handlePointerLeave(); + const onPointerDown = (event: PointerEvent) => this.handlePointerDown(event); + const onPointerUp = (event: PointerEvent) => this.handlePointerUp(event); + const onPointerCancel = () => this.handlePointerLeave(); + + const onMouseMove = (event: MouseEvent) => this.handleMouseMove(event); + const onMouseLeave = () => this.handlePointerLeave(); + const onMouseDown = (event: MouseEvent) => this.handleMouseDown(event); + const onMouseUp = () => this.handleMouseUp(); + + const onTouchStart = (event: TouchEvent) => this.handleTouchStart(event); + const onTouchMove = (event: TouchEvent) => this.handleTouchMove(event); + const onTouchEnd = (event: TouchEvent) => this.handleTouchEnd(event); + const onTouchCancel = () => this.handlePointerLeave(); + const onMotionChange = (event: MediaQueryListEvent) => this.handleMotionPreferenceChange(event.matches); window.addEventListener('resize', onResize); - window.addEventListener('pointermove', onMove, { passive: true }); - window.addEventListener('pointerleave', onLeave); - window.addEventListener('pointerdown', onDown, { passive: true }); - window.addEventListener('pointerup', onUp, { passive: true }); + + if (supportsPointerEvents) { + window.addEventListener('pointermove', onPointerMove, { passive: true }); + window.addEventListener('pointerleave', onPointerLeave); + window.addEventListener('pointerdown', onPointerDown, { passive: true }); + window.addEventListener('pointerup', onPointerUp, { passive: true }); + window.addEventListener('pointercancel', onPointerCancel, { passive: true }); + } else { + window.addEventListener('mousemove', onMouseMove, { passive: true }); + window.addEventListener('mouseleave', onMouseLeave); + window.addEventListener('mousedown', onMouseDown, { passive: true }); + window.addEventListener('mouseup', onMouseUp, { passive: true }); + window.addEventListener('touchstart', onTouchStart, { passive: true }); + window.addEventListener('touchmove', onTouchMove, { passive: true }); + window.addEventListener('touchend', onTouchEnd, { passive: true }); + window.addEventListener('touchcancel', onTouchCancel, { passive: true }); + } + this.mediaQuery.addEventListener('change', onMotionChange); this.destroyCallbacks = [ () => window.removeEventListener('resize', onResize), - () => window.removeEventListener('pointermove', onMove), - () => window.removeEventListener('pointerleave', onLeave), - () => window.removeEventListener('pointerdown', onDown), - () => window.removeEventListener('pointerup', onUp), + () => window.removeEventListener('pointermove', onPointerMove), + () => window.removeEventListener('pointerleave', onPointerLeave), + () => window.removeEventListener('pointerdown', onPointerDown), + () => window.removeEventListener('pointerup', onPointerUp), + () => window.removeEventListener('pointercancel', onPointerCancel), + () => window.removeEventListener('mousemove', onMouseMove), + () => window.removeEventListener('mouseleave', onMouseLeave), + () => window.removeEventListener('mousedown', onMouseDown), + () => window.removeEventListener('mouseup', onMouseUp), + () => window.removeEventListener('touchstart', onTouchStart), + () => window.removeEventListener('touchmove', onTouchMove), + () => window.removeEventListener('touchend', onTouchEnd), + () => window.removeEventListener('touchcancel', onTouchCancel), () => this.mediaQuery.removeEventListener('change', onMotionChange), ]; } @@ -282,18 +319,115 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy { } private handlePointerMove(event: PointerEvent): void { - this.pointerX = event.clientX; - this.pointerY = event.clientY; - this.pointerActive = true; - this.cursorTransform = `translate3d(${event.clientX}px, ${event.clientY}px, 0)`; + if (event.pointerType === 'touch' && this.activeTouchId !== null && event.pointerId !== this.activeTouchId) { + return; + } + + this.updatePointer(event.clientX, event.clientY, true); + } + + private handlePointerDown(event: PointerEvent): void { + if (event.pointerType === 'touch') { + this.activeTouchId = event.pointerId; + } + + this.isPointerDown = true; + this.updatePointer(event.clientX, event.clientY, true); + } + + private handlePointerUp(event: PointerEvent): void { + this.isPointerDown = false; + + if (event.pointerType === 'touch') { + if (this.activeTouchId !== null && event.pointerId !== this.activeTouchId) { + return; + } + + this.activeTouchId = null; + this.pointerActive = false; + return; + } + + this.updatePointer(event.clientX, event.clientY, true); + } + + private handleMouseMove(event: MouseEvent): void { + this.updatePointer(event.clientX, event.clientY, true); + } + + private handleMouseDown(event: MouseEvent): void { + this.isPointerDown = true; + this.updatePointer(event.clientX, event.clientY, true); + } + + private handleMouseUp(): void { + this.isPointerDown = false; + } + + private handleTouchStart(event: TouchEvent): void { + const touch = event.changedTouches.item(0); + if (!touch) { + return; + } + + this.activeTouchId = touch.identifier; + this.isPointerDown = true; + this.updatePointer(touch.clientX, touch.clientY, true); + } + + private handleTouchMove(event: TouchEvent): void { + if (this.activeTouchId === null) { + return; + } + + const touch = this.findTouch(event.touches, this.activeTouchId); + if (!touch) { + return; + } + + this.updatePointer(touch.clientX, touch.clientY, true); + } + + private handleTouchEnd(event: TouchEvent): void { + if (this.activeTouchId === null) { + return; + } + + const touch = this.findTouch(event.changedTouches, this.activeTouchId); + if (!touch) { + return; + } + + this.isPointerDown = false; + this.activeTouchId = null; + this.pointerActive = false; } private handlePointerLeave(): void { this.pointerActive = false; this.isPointerDown = false; + this.activeTouchId = null; this.cursorTransform = 'translate3d(-9999px, -9999px, 0)'; } + private updatePointer(x: number, y: number, active: boolean): void { + this.pointerX = x; + this.pointerY = y; + this.pointerActive = active; + this.cursorTransform = `translate3d(${x}px, ${y}px, 0)`; + } + + private findTouch(touches: TouchList, identifier: number): Touch | null { + for (let index = 0; index < touches.length; index += 1) { + const touch = touches.item(index); + if (touch && touch.identifier === identifier) { + return touch; + } + } + + return null; + } + private syncPointer(): void { this.particleEngine?.setPointer({ x: this.pointerX, @@ -342,3 +476,4 @@ export class InteractiveBackground implements AfterViewInit, OnDestroy { +