Skip to content

Instantly share code, notes, and snippets.

@cbejensen
Created October 22, 2019 09:00
Show Gist options
  • Save cbejensen/f2f8cfdaebf27ca858175a7c67741cda to your computer and use it in GitHub Desktop.
Save cbejensen/f2f8cfdaebf27ca858175a7c67741cda to your computer and use it in GitHub Desktop.
Angular Service for Wheelzoom JS library
// Service for https://github.com/jackmoore/wheelzoom
import { Injectable, NgZone } from '@angular/core';
export interface WheelzoomSettings {
zoom: number;
maxZoom?: number;
initialZoom: number;
initialX: number;
initialY: number;
}
@Injectable()
export class WheelzoomService {
private _defaults: WheelzoomSettings = {
zoom: 0.1,
initialZoom: 1,
initialX: 0.5,
initialY: 0.5,
};
private _originalProperties: {
backgroundImage: any;
backgroundRepeat: any;
src: any;
};
private _img: HTMLImageElement;
private _settings: WheelzoomSettings;
private _width: number;
private _height: number;
private _bgWidth: number;
private _bgHeight: number;
private _bgPosX: number;
private _bgPosY: number;
private _previousEvent: { pageX: number; pageY: number };
private _transparentSpaceFiller: string;
constructor(private zone: NgZone) {}
attach = (img: HTMLImageElement, settings: Partial<WheelzoomSettings> = {}) => {
if (!img || !img.nodeName || img.nodeName !== 'IMG') {
return;
}
this.zone.runOutsideAngular(() => {
this._img = img;
this._settings = {
...this._defaults,
...settings,
};
this._originalProperties = {
backgroundImage: img.style.backgroundImage,
backgroundRepeat: img.style.backgroundRepeat,
src: img.src,
};
img.addEventListener('wheelzoom.destroy', this.destroy);
if (img.complete) {
this._load();
}
img.addEventListener('load', this._load);
});
};
destroy = () => {
this._img.removeEventListener('wheelzoom.destroy', this.destroy);
this._img.removeEventListener('wheelzoom.reset', this._reset);
this._img.removeEventListener('load', this._load);
this._img.removeEventListener('mouseup', this._removeDrag);
this._img.removeEventListener('mousemove', this._drag);
this._img.removeEventListener('mousedown', this._draggable);
this._img.removeEventListener('wheel', this._onwheel);
this._img.style.backgroundImage = this._originalProperties.backgroundImage;
this._img.style.backgroundRepeat = this._originalProperties.backgroundRepeat;
this._img.src = this._originalProperties.src;
};
private _load = () => {
const initial = Math.max(this._settings.initialZoom, 1);
if (this._img.src === this._transparentSpaceFiller) return;
const computedStyle = window.getComputedStyle(this._img, null);
this._width = parseInt(computedStyle.width, 10);
this._height = parseInt(computedStyle.height, 10);
this._bgWidth = this._width * initial;
this._bgHeight = this._height * initial;
this._bgPosX = -(this._bgWidth - this._width) * this._settings.initialX;
this._bgPosY = -(this._bgHeight - this._height) * this._settings.initialY;
this._setSrcToBackground();
this._img.style.backgroundSize = this._bgWidth + 'px ' + this._bgHeight + 'px';
this._img.style.backgroundPosition = this._bgPosX + 'px ' + this._bgPosY + 'px';
this._img.addEventListener('wheelzoom.reset', this._reset);
this._img.addEventListener('wheel', this._onwheel);
this._img.addEventListener('mousedown', this._draggable);
};
private _setSrcToBackground = () => {
this._img.style.backgroundRepeat = 'no-repeat';
this._img.style.backgroundImage = 'url("' + this._img.src + '")';
this._transparentSpaceFiller =
'data:image/svg+xml;base64,' +
window.btoa(
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
this._img.naturalWidth +
'" height="' +
this._img.naturalHeight +
'"></svg>'
);
this._img.src = this._transparentSpaceFiller;
};
private _updateBgStyle = () => {
if (this._bgPosX > 0) {
this._bgPosX = 0;
} else if (this._bgPosX < this._width - this._bgWidth) {
this._bgPosX = this._width - this._bgWidth;
}
if (this._bgPosY > 0) {
this._bgPosY = 0;
} else if (this._bgPosY < this._height - this._bgHeight) {
this._bgPosY = this._height - this._bgHeight;
}
this._img.style.backgroundSize = this._bgWidth + 'px ' + this._bgHeight + 'px';
this._img.style.backgroundPosition = this._bgPosX + 'px ' + this._bgPosY + 'px';
};
private _reset = () => {
this._bgWidth = this._width;
this._bgHeight = this._height;
this._bgPosX = this._bgPosY = 0;
this._updateBgStyle();
};
private _onwheel = (e: WheelEvent) => {
e.preventDefault();
const deltaY = e.deltaY;
// As far as I know, there is no good cross-browser way to get the cursor position relative to the event target.
// We have to calculate the target element's position relative to the document, and subtract that from the
// cursor's position relative to the document.
const rect = this._img.getBoundingClientRect();
const offsetX = e.pageX - rect.left - window.pageXOffset;
const offsetY = e.pageY - rect.top - window.pageYOffset;
// Record the offset between the bg edge and cursor:
const bgCursorX = offsetX - this._bgPosX;
const bgCursorY = offsetY - this._bgPosY;
// Use the previous offset to get the percent offset between the bg edge and cursor:
const bgRatioX = bgCursorX / this._bgWidth;
const bgRatioY = bgCursorY / this._bgHeight;
// Update the bg size:
if (deltaY < 0) {
this._bgWidth += this._bgWidth * this._settings.zoom;
this._bgHeight += this._bgHeight * this._settings.zoom;
} else {
this._bgWidth -= this._bgWidth * this._settings.zoom;
this._bgHeight -= this._bgHeight * this._settings.zoom;
}
if (this._settings.maxZoom) {
this._bgWidth = Math.min(this._width * this._settings.maxZoom, this._bgWidth);
this._bgHeight = Math.min(this._height * this._settings.maxZoom, this._bgHeight);
}
// Take the percent offset and apply it to the new size:
this._bgPosX = offsetX - this._bgWidth * bgRatioX;
this._bgPosY = offsetY - this._bgHeight * bgRatioY;
// Prevent zooming out beyond the starting size
if (this._bgWidth <= this._width || this._bgHeight <= this._height) {
this._reset();
} else {
this._updateBgStyle();
}
};
private _drag = (e: DragEvent) => {
e.preventDefault();
this._bgPosX += e.pageX - this._previousEvent.pageX;
this._bgPosY += e.pageY - this._previousEvent.pageY;
this._previousEvent = e;
this._updateBgStyle();
};
private _removeDrag = () => {
document.removeEventListener('mouseup', this._removeDrag);
document.removeEventListener('mousemove', this._drag);
};
// Make the background draggable
private _draggable = (e: DragEvent) => {
e.preventDefault();
this._previousEvent = e;
document.addEventListener('mousemove', this._drag);
document.addEventListener('mouseup', this._removeDrag);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment