# ngx-pinch-zoom [![Angular](https://img.shields.io/badge/Angular-20-red.svg)](https://angular.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue.svg)](https://www.typescriptlang.org/) An Angular library for pinch-to-zoom functionality on touch-enabled devices and mouse interactions. Built with Angular 20+ and modern signals API. ## Features - 🎯 **Angular 20+ with Signals** - Modern reactive programming - πŸ“± **Touch & Mouse Support** - Works on all devices - πŸ”„ **Pinch to Zoom** - Natural gesture support - πŸ–±οΈ **Mouse Wheel Zoom** - Desktop-friendly - πŸ‘† **Double Tap** - Quick zoom in/out - 🎯 **Click to Zoom** - Click any point to zoom in precisely - β˜€οΈ **Brightness Control** - Adjust image brightness on the fly - 🎨 **Highly Configurable** - Extensive options - πŸ“¦ **Standalone Component** - No module imports needed - ⚑ **Performance Optimized** - Uses signals for reactivity ## Installation ```bash npm install @brianpooe/ngx-pinch-zoom ``` ## Quick Start ### 1. Import the Component ```typescript import { Component } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-root', standalone: true, imports: [PinchZoomComponent], template: ` Zoomable image `, }) export class AppComponent {} ``` ### 2. Add Viewport Meta Tag For proper touch support, add this to your `index.html`: ```html ``` ## Usage Examples ### Basic Usage ```html ``` ### With Configuration (Using Signals) ```typescript import { Component, signal } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-example', standalone: true, imports: [PinchZoomComponent], template: ` `, }) export class ExampleComponent { isDisabled = signal(false); onZoomChange(scale: number) { console.log('Current zoom level:', scale); } } ``` ### Programmatic Control ```typescript import { Component, viewChild } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-controls', standalone: true, imports: [PinchZoomComponent], template: ` `, }) export class ControlsComponent { pinchZoom = viewChild('pinchZoom'); zoomIn() { this.pinchZoom()?.zoomIn(0.5); } zoomOut() { this.pinchZoom()?.zoomOut(0.5); } reset() { this.pinchZoom()?.toggleZoom(); } } ``` ### Click to Zoom Enable click-to-zoom for quick inspection of specific areas: ```typescript import { Component } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-click-zoom', standalone: true, imports: [PinchZoomComponent], template: ` `, }) export class ClickZoomComponent {} ``` This is particularly useful for: - **Anomaly detection** - Click on suspicious areas to zoom in - **Defect inspection** - Quickly examine specific spots - **Detail review** - Fast workflow for inspecting multiple points When enabled: - Click any point on the image to zoom to that exact location - Click again to zoom out back to original view - Cursor changes to zoom-in icon to indicate the feature is active ### Brightness Control Enable brightness controls with a single toggle button: ```typescript import { Component } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-brightness', standalone: true, imports: [PinchZoomComponent], template: ` `, }) export class BrightnessComponent { onBrightnessChange(brightness: number) { console.log('Current brightness:', brightness); } } ``` **How the brightness button works:** - Click repeatedly to increase brightness by `brightnessStep` increments - Icon changes from outline sun (normal) to filled sun (brightened) - When max brightness is reached, clicking resets back to normal (1.0) - Consistent UX pattern with the zoom control Programmatic brightness control: ```typescript import { Component, viewChild } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-brightness-controls', standalone: true, imports: [PinchZoomComponent], template: ` `, }) export class BrightnessControlsComponent { pinchZoom = viewChild('pinchZoom'); brightnessIn() { this.pinchZoom()?.brightnessIn(); } brightnessOut() { this.pinchZoom()?.brightnessOut(); } resetBrightness() { this.pinchZoom()?.resetBrightness(); } } ``` ### Custom Controls with Plus/Minus Buttons Build your own custom UI by disabling default controls and using the public methods with smart min/max behavior: ```typescript import { Component, viewChild, signal, computed } from '@angular/core'; import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom'; @Component({ selector: 'app-custom-controls', standalone: true, imports: [PinchZoomComponent], template: `

Zoom: {{ zoomLevel() }} | Brightness: {{ brightnessLevel() }}

`, }) export class CustomControlsComponent { pinchZoom = viewChild('pinchZoom'); zoomLevel = signal(1); brightnessLevel = signal(1.0); // Min/max constants private readonly MIN_ZOOM = 1; private readonly MIN_BRIGHTNESS = 1.0; private readonly MAX_BRIGHTNESS = 2.0; // Computed signals for button states isZoomAtMin = computed(() => this.zoomLevel() <= this.MIN_ZOOM); isBrightnessAtMin = computed(() => this.brightnessLevel() <= this.MIN_BRIGHTNESS); // Smart handlers with min/max and reset behavior handleZoomIn() { const pinch = this.pinchZoom(); if (!pinch) return; const maxScale = pinch.maxScale(); if (this.zoomLevel() >= maxScale) { // At max, reset to min pinch.destroy(); } else { pinch.zoomIn(0.5); } } handleZoomOut() { const pinch = this.pinchZoom(); if (!pinch || this.isZoomAtMin()) return; pinch.zoomOut(0.5); } handleBrightnessIn() { const pinch = this.pinchZoom(); if (!pinch) return; if (this.brightnessLevel() >= this.MAX_BRIGHTNESS) { // At max, reset to normal pinch.resetBrightness(); } else { pinch.brightnessIn(); } } handleBrightnessOut() { const pinch = this.pinchZoom(); if (!pinch || this.isBrightnessAtMin()) return; pinch.brightnessOut(); } onZoomChange(scale: number) { this.zoomLevel.set(scale); } onBrightnessChange(brightness: number) { this.brightnessLevel.set(brightness); } } ``` **Smart Control Behavior:** - **Minus buttons**: Disabled when at minimum value (initial state), prevent going below min - **Plus buttons**: When at maximum value, clicking resets to minimum instead of being disabled - This pattern provides intuitive UX - users can't accidentally go below min, and can easily reset from max ## Configuration Options | Input | Type | Default | Description | | ------------------------- | --------------------------------- | ----------------------- | ----------------------------------------- | | `transitionDuration` | `number` | `200` | Animation duration in milliseconds | | `doubleTap` | `boolean` | `true` | Enable double-tap to zoom | | `doubleTapScale` | `number` | `2` | Scale factor for double-tap zoom | | `autoZoomOut` | `boolean` | `false` | Automatically reset zoom after pinch | | `limitZoom` | `number \| 'original image size'` | `'original image size'` | Maximum zoom level | | `minScale` | `number` | `0` | Minimum allowed scale | | `disabled` | `boolean` | `false` | Disable all zoom functionality | | `disablePan` | `boolean` | `false` | Disable panning with one finger | | `disableZoomControl` | `'disable' \| 'never' \| 'auto'` | `'auto'` | Control zoom button visibility | | `overflow` | `'hidden' \| 'visible'` | `'hidden'` | CSS overflow behavior | | `zoomControlScale` | `number` | `1` | Scale factor for zoom controls | | `backgroundColor` | `string` | `'rgba(0,0,0,0.85)'` | Container background color | | `limitPan` | `boolean` | `false` | Prevent panning past image edges | | `minPanScale` | `number` | `1.0001` | Minimum scale at which panning is enabled | | `listeners` | `'auto' \| 'mouse and touch'` | `'mouse and touch'` | Event listener mode | | `wheel` | `boolean` | `true` | Enable mouse wheel zoom | | `wheelZoomFactor` | `number` | `0.2` | Zoom factor for mouse wheel | | `autoHeight` | `boolean` | `false` | Calculate height from image dimensions | | `draggableImage` | `boolean` | `false` | Make image draggable | | `draggableOnPinch` | `boolean` | `false` | Allow dragging while pinching | | `enableBrightnessControl` | `boolean` | `false` | Enable brightness adjustment controls | | `brightnessStep` | `number` | `0.1` | Brightness adjustment increment | | `minBrightness` | `number` | `0.1` | Minimum brightness value | | `maxBrightness` | `number` | `2.0` | Maximum brightness value | | `enableClickToZoom` | `boolean` | `false` | Enable click-to-zoom functionality | | `clickToZoomScale` | `number` | `2.5` | Target scale when clicking to zoom | ## Outputs | Output | Type | Description | | ------------------- | -------------------------- | ---------------------------------------- | | `zoomChanged` | `OutputEmitterRef` | Emits current scale when zoom changes | | `brightnessChanged` | `OutputEmitterRef` | Emits current brightness when it changes | ## Methods Access these methods via template reference or `viewChild`: | Method | Parameters | Returns | Description | | -------------------- | ------------------- | -------- | --------------------------------------------------- | | `toggleZoom()` | - | `void` | Toggle between zoomed in/out | | `zoomIn(value)` | `value: number` | `number` | Zoom in by value, returns new scale | | `zoomOut(value)` | `value: number` | `number` | Zoom out by value, returns new scale | | `brightnessIn()` | - | `number` | Increase brightness by step, returns new brightness | | `brightnessOut()` | - | `number` | Decrease brightness by step, returns new brightness | | `resetBrightness()` | - | `void` | Reset brightness to default (1.0) | | `zoomToPoint(event)` | `event: MouseEvent` | `void` | Zoom to the clicked point (used internally) | | `destroy()` | - | `void` | Clean up event listeners | ## Computed Properties The component exposes several computed signals: | Property | Type | Description | | ----------------------- | --------- | ------------------------------------------- | | `scale()` | `number` | Current zoom scale | | `isZoomedIn()` | `boolean` | Whether image is zoomed in | | `isDisabled()` | `boolean` | Whether zoom is disabled | | `isDragging()` | `boolean` | Whether user is currently dragging | | `isZoomLimitReached()` | `boolean` | Whether max zoom is reached | | `maxScale()` | `number` | Maximum allowed scale | | `isControl()` | `boolean` | Whether zoom controls should be shown | | `brightness()` | `number` | Current brightness value | | `isBrightnessControl()` | `boolean` | Whether brightness controls should be shown | | `isBrightnessAtMin()` | `boolean` | Whether brightness is at minimum | | `isBrightnessAtMax()` | `boolean` | Whether brightness is at maximum | ## Angular 20 Signals This library fully embraces Angular 20's signals API: ### Input Signals All component inputs are now signal-based for better performance and reactivity. ```typescript // Before (Angular <16) @Input() disabled: boolean = false; // Now (Angular 20+) disabled = input(false); ``` ### Output Signals Outputs use the new output() API: ```typescript // Before @Output() zoomChanged = new EventEmitter(); // Now zoomChanged = output(); ``` ### Computed Signals Derived state uses computed signals: ```typescript isZoomedIn = computed(() => { return this.scale() > 1; }); ``` ## Browser Support - Chrome/Edge (latest 2 versions) - Firefox (latest 2 versions) - Safari (latest 2 versions) - iOS Safari (latest 2 versions) - Chrome for Android (latest 2 versions) ## Requirements - Angular 20.0.0 or higher - TypeScript 5.8.0 or higher - Node.js 18.19.1, 20.11.1, or 22.0.0+ ## Migration from Older Versions If you're upgrading from a pre-signals version: 1. **Inputs**: No changes needed in templates, binding syntax remains the same 2. **Outputs**: Event binding syntax remains the same 3. **ViewChild**: Update to `viewChild` signal (optional but recommended) 4. **Component properties**: Access computed properties by calling them: `component.scale()` ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines. ## Architecture This library follows Angular best practices with a clean, professional architecture: ### Directory Structure ``` lib/ β”œβ”€β”€ models/ # Data models and interfaces β”‚ β”œβ”€β”€ interfaces.model.ts β”‚ β”œβ”€β”€ properties.model.ts β”‚ β”œβ”€β”€ zoom-config.model.ts β”‚ β”œβ”€β”€ transform-state.model.ts β”‚ β”œβ”€β”€ brightness-state.model.ts β”‚ └── click-to-zoom.model.ts β”‚ β”œβ”€β”€ services/ # Business logic β”‚ β”œβ”€β”€ brightness.service.ts # Angular service for brightness state β”‚ β”œβ”€β”€ zoom-state.service.ts # Angular service for zoom state β”‚ β”œβ”€β”€ ivy-pinch.service.ts # Core zoom/pan logic β”‚ └── touches.service.ts # Gesture detection β”‚ └── components/ β”œβ”€β”€ containers/ # Smart components (with DI) β”‚ └── pinch-zoom/ β”‚ β”œβ”€β”€ pinch-zoom.container.ts β”‚ β”œβ”€β”€ pinch-zoom.container.html β”‚ └── pinch-zoom.container.sass β”‚ └── presentational/ # Dumb components (pure UI) β”œβ”€β”€ zoom-controls/ └── brightness-controls/ ``` ### Design Patterns - **Smart/Dumb Component Pattern**: Clear separation between container components (business logic) and presentational components (pure UI) - **Service-Based Architecture**: Business logic extracted into reusable Angular services - **Signal-Based Reactivity**: All state management uses Angular 20 signals for optimal performance - **Dependency Injection**: Proper use of Angular's DI system throughout ## License MIT ## Credits This library is built on the foundation of the original ngx-pinch-zoom, modernized for Angular 20 with extensive architectural improvements and new features. **What's From the Original:** - Core pinch-to-zoom mathematics and transform algorithms (IvyPinch) - Touch and mouse gesture detection logic (Touches) - Original zoom/pan calculations and constraints **What's New in This Version:** - Complete Angular 20 signals API migration (inputs, outputs, computed, effects) - Professional architecture (models/, services/, containers/, presentational/) - Smart/Dumb component pattern with explicit separation - New features: brightness control, click-to-zoom - Service-based state management (BrightnessService, ZoomStateService) - 778 lines of comprehensive JSDoc documentation - Modern TypeScript with full strict mode compliance **Original Library:** - **Author:** [Nikita Drozhzhin](https://github.com/drozhzhin-n-e) - Original creator and core zoom algorithms - **Repository:** [drozhzhin-n-e/ngx-pinch-zoom](https://github.com/drozhzhin-n-e/ngx-pinch-zoom) **Angular 19/20 Compatibility Fork:** - **Contributors:** - [Konstantin SchΓΌtte](https://www.meddv.de) (medDV-GmbH) - Angular 19/20 compatibility updates - [BjΓΆrn Schmidt](https://www.meddv.de) (medDV-GmbH) - Angular 19/20 compatibility updates - **Repository:** [medDV-GmbH/ngx-pinch-zoom](https://github.com/medDV-GmbH/ngx-pinch-zoom) **Current Version:** - **Maintainer:** [Brian Pooe](https://github.com/brianpooe) - Angular 20 signals migration, architecture redesign, new features (brightness control, click-to-zoom), comprehensive documentation ## Issues and Support Please report issues on [GitHub Issues](https://github.com/brianpooe/ngx-pinch-zoom/issues)