How to Implement Pinch to Zoom on the Browser in Angular

Vikram Thyagarajan
Level Up Coding
Published in
4 min readMay 6, 2021

--

Photo by Sigmund on Unsplash

At Invideo, we’re always looking to give our customers the best user experience to empower them to make their best videos. A key aspect of this is their workflow around the timeline

For video editors, the timeline is the most integral aspect of the tool that they use. Users intuitively pinch the touchpad or trackpad that they work on to see if the timeline zoom works. It is such an intrinsic part of their creation workflow, that muscle memory kicks in

However, pinch to zoom on desktop browsers lacks cohesive support. This interaction is not part of any spec, which makes it very difficult to implement. However, there are workarounds so you can get a good enough pinch zoom experience for all

The Problem

The trackpad as a concept is a very weird one when it comes to browsers and its implementation

"Are trackpad events mouse events? Are they touch events? Are they somewhere in between"- Buddha, post enlightenment, getting utterly confused by javascript

Since there is no new event that can completely categorise the trackpad events, the browsers have found workarounds. 2 finger movements have been mapped to the scroll event. Some OS’s for example captures a 3 finger touch and sends the right click event to the browser

How does this relate to Pinch to Zoom? Well, some browsers have employed workarounds with respect to the pinch interaction as well. The first one is to send the mousewheel event with the ctrlKey value set to true. The default behaviour for this is to zoom the whole screen, but by calling preventDefault we can override this. Reference links to understand this at the bottom

The browser support seems to be as following:

Safari instead handles this using a proprietary GestureEvent that is triggered with a scale value that is triggered when a pinch is performed by the user. More information here

Implementation in Angular

Now that we’ve understood the basics and even a little bit of the history of this support, let’s dive into some code. How we’ve utilized this at Invideo is to implement a stepped zoom functionality on our video timeline. It kind of looks like this

Zoom Barabar Zoom

Our timeline is dynamic and javascript based, so a directive would be best to implement our pinch zoom logic. An implementation would kind of look like this.

import { Directive, HostListener, Input, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs';
import { clamp } from 'lodash';
@Directive({
selector: '[appPinchZoom]',
})
export class PinchZoomDirective implements OnInit {
@Input() scaleFactor: number = 0.08;
@Input() zoomThreshold: number = 9;
@Input() initialZoom: number = 5;
@Input() debounceTime: number = 100; // in ms
scale: number;
@Output() onPinch$: Subject<number> = new Subject<number>();
constructor() {

}
ngOnInit(): void {
this.scale = this.initialZoom;
}
@HostListener('wheel', ['$event'])
onWheel($event: WheelEvent) {
if (!$event.ctrlKey) return;
$event.preventDefault();
let scale = this.scale - $event.deltaY * this.scaleFactor;
scale = clamp(scale, 1, this.zoomThreshold);
this.calculatePinch(scale);
}
calculatePinch(scale: number) {
this.scale = scale;
this.onPinch$.next(this.scale));
}
}

This directive calculates pinch logic, calculates when the scale has gone up a step and sends in an output event onPinch. The main code of this is in the onWheel function, which takes the mousewheel event, listens to the ctrlKey value and based on a scaleFactor that controls sensitivity.

Using this is as simple as just calling this directive in a component and setting the sensitivity factor

// timeline.component.html
<div
class="timeline"
#timeline
appPinchZoom
(onPinch$)="onPinch($event)"
[scaleFactor]="0.02"
[zoomThreshold]="9"
[initialZoom]="5"
>
<!-- timeline internals go here -->
</div>// timeline.component.ts
@Component({
selector: 'app-timeline-v2',
templateUrl: './timeline-v2.component.html',
styleUrls: ['./timeline-v2.component.scss'],
})
export class TimelineComponent{
constructor(private timelineService: TimelineService) {}
onPinch(level: number) {
this.timelineService.updateZoom(level);
}
}

An astute observer would see that we have only implemented this functionality for the Chrome , Firefox and Edge, which has implemented the ctrlKey fix. So what do we do for Safari? Thank you, I thought you’d never ask even though I’ve been dying to tell.

  @HostListener('gesturestart', ['$event'])
@HostListener('gesturechange', ['$event'])
@HostListener('gestureend', ['$event'])
onGesture($event: any) {
$event.preventDefault();
let pinchAmount = $event.scale - 1;
let scale = this.scale + pinchAmount * this.scaleFactor;
scale = clamp(scale, 1, this.zoomThreshold + this.thresholdBuffer);
this.calculatePinch(scale);
}

And you’re good to go. It’s time to boast about this by writing a blog post 😉

Possible improvements

This article covers the basics of implementing the pinch zoom for browsers, but not much about the best way to use this. What is going on inside our onPinch function, how do we render our elements on the timeline so that it is not janky? How can we optimise performance? Stay tuned on the next episode of “Vikram Groks The Internet”, coming soon to a Medium publication near you

References

--

--

Software Developer with clients like PepsiCo, Unilever and McKinsey & Co. Passionate about Technology, Travel and Music. Speaks of self in 3rd person.