How to Implement Pinch to Zoom on the Browser in Angular

InVideo
InSide InVideo
Published in
5 min readMay 10, 2021

Vikram Thyagarajan, our senior full-stack engineer pens down his thoughts on the basics of implementing the pinch-zoom function for browsers. Here to make your video creation journey smoother than ever!

Image 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 their 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. Two finger movements have been mapped to the scroll event. Some OS’s for example capture 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 behavior 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 on Pinch. The main code of this is in the onWheel function, which takes the mouse-wheel event, listens to the ctrlKey value and based on a scale Factor 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 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 optimize performance? Stay tuned on the next episode of “Vikram Groks The Internet,” coming soon to a Medium publication near you.

This article was originally published here and is authored by Vikram Thyagarajan.

References

--

--

InVideo
InSide InVideo

The future of video creation is in the browser, across devices, collaborative and easy. With users in 150+ countries, here’s how we’re scaling InVideo.