Skip to content

Instantly share code, notes, and snippets.

@OysteinAmundsen
Created March 14, 2025 12:56
Show Gist options
  • Save OysteinAmundsen/c812a8e055b59bd1c7232455b2c72b15 to your computer and use it in GitHub Desktop.
Save OysteinAmundsen/c812a8e055b59bd1c7232455b2c72b15 to your computer and use it in GitHub Desktop.
Hijack browser pinch-zoom and report back delta zoom level
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core';
export interface ZoomEvent {
direction: 'in' | 'out';
delta: number;
scrollLeft?: number;
scrollTop?: number;
}
export interface ZoomOptions {
zoomLevel: number;
maxZoom: number;
minZoom: number;
}
/**
* This directive detects a 2-pointer horizontal pinch/zoom gesture.
*
* If the distance between the two pointers has increased, zoom in.
* And if the distance is decreasing, zoom out
*
* The directive also listens for wheel events and will zoom in/out
* if the CTRL key is also pressed.
*/
@Directive({
selector: '[libPinchZoom]',
standalone: true,
})
export class PinchZoomDirective {
startDistance = 0;
currentDistance = 0;
_options = {
zoomLevel: 1,
maxZoom: 10,
minZoom: 1,
};
@Input(`libPinchZoom`) set options(options: Partial<ZoomOptions>) {
Object.assign(this._options, options);
}
zoomDelta = 1;
oldZoom = 1;
clientX = 0;
clientY = 0;
isFocused = false;
@Output() zoomed = new EventEmitter<ZoomEvent>();
constructor(private el: ElementRef<HTMLElement>) {}
@HostListener('touchstart', ['$event'])
onTouchStart($event: TouchEvent) {
if ($event.touches.length === 2) {
// Calculate the distance between the two touch points
const x = $event.touches[0].clientX - $event.touches[1].clientX;
const y = $event.touches[0].clientY - $event.touches[1].clientY;
this.startDistance = Math.sqrt(x * x + y * y);
// Calculate position of pointer relative to element
const size = this.el.nativeElement.getBoundingClientRect();
this.clientX = x - size.x;
this.clientY = y - size.y;
}
}
@HostListener('touchmove', ['$event'])
onTouchMove($event: TouchEvent) {
if ($event.touches.length === 2) {
let direction: 'in' | 'out';
// Calculate the current distance between the touch points
const x = $event.touches[0].clientX - $event.touches[1].clientY;
const y = $event.touches[0].clientY - $event.touches[1].clientY;
this.currentDistance = Math.sqrt(x * x + y * y);
// Calculate position of pointer relative to element
const size = this.el.nativeElement.getBoundingClientRect();
this.clientX = x - size.x;
this.clientY = y - size.y;
// Calculate the amount of zoom
const zoomDelta = this.currentDistance / this.startDistance;
// eslint-disable-next-line prefer-const
direction = this.zoomDelta < zoomDelta ? 'in' : 'out';
this.zoomDelta = zoomDelta;
this.onZoom(direction);
// Prevent the browser's default zoom behavior
$event.preventDefault();
}
}
@HostListener('pointerleave')
onLeave() {
this.isFocused = false;
}
@HostListener('pointerover')
onEnter() {
this.isFocused = true;
}
@HostListener('wheel', ['$event'])
onWheel($event: WheelEvent) {
if ($event.ctrlKey) {
// Calculate position of pointer relative to element
const size = this.el.nativeElement.getBoundingClientRect();
this.clientX = $event.clientX - size.x;
this.clientY = $event.clientY - size.y;
// Check if the user is scrolling up or down
let direction: 'in' | 'out';
if ($event.deltaY < 0) {
direction = 'in';
this.zoomDelta += 0.1;
} else {
direction = 'out';
this.zoomDelta -= 0.1;
}
this.onZoom(direction);
// Prevent the browser's default zoom behavior
$event.preventDefault();
}
}
@HostListener('document:keydown', ['$event'])
keydown(evt: KeyboardEvent) {
if (this.isFocused && evt.ctrlKey && ['+', '-', '0'].includes(evt.key)) {
evt.preventDefault();
evt.stopPropagation();
switch (evt.key) {
case '+':
this.zoomDelta += 0.1;
return this.onZoom('in');
case '-':
this.zoomDelta -= 0.1;
return this.onZoom('out');
case '0':
const direction = this.zoomDelta > 1 ? 'out' : 'in';
this.zoomDelta = 1;
return this.onZoom(direction);
}
}
}
onZoom(direction: 'in' | 'out') {
if (this.zoomDelta < this._options.minZoom) {
this.zoomDelta = this._options.minZoom;
} else if (this.zoomDelta > this._options.maxZoom) {
this.zoomDelta = this._options.maxZoom;
} else {
const zoom = this.zoomDelta / this.oldZoom;
const scrollLeft = (this.clientX + this.el.nativeElement.scrollLeft) * zoom - this.clientX;
const scrollTop = (this.clientY + this.el.nativeElement.scrollTop) * zoom - this.clientY;
this.oldZoom = this.zoomDelta;
// Notify subscribers
this.zoomed.emit({
direction,
delta: this.zoomDelta,
...(this.clientX && { scrollLeft }),
...(this.clientY && { scrollTop }),
});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment