Improve custom controls example with intelligent button behavior: - Minus buttons disabled when at minimum (initial state) - Plus buttons reset to minimum when at maximum - Prevents going below min values - Provides intuitive UX with disabled states Implementation: - Added computed signals: isZoomAtMin, isBrightnessAtMin, isBrightnessAtMax - Created handler methods: handleZoomIn/Out, handleBrightnessIn/Out - Handlers check limits and reset when appropriate - Template binds [disabled] to computed signals Benefits: - Users can't accidentally go below minimum - Easy reset from maximum with single click - Clear visual feedback via disabled buttons - Clean pattern for custom toolbar integration
20 KiB
ngx-pinch-zoom
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
npm install @brianpooe/ngx-pinch-zoom
Quick Start
1. Import the Component
import { Component } from '@angular/core';
import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom';
@Component({
selector: 'app-root',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom>
<img src="path/to/image.jpg" alt="Zoomable image" />
</pinch-zoom>
`,
})
export class AppComponent {}
2. Add Viewport Meta Tag
For proper touch support, add this to your index.html:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
Usage Examples
Basic Usage
<pinch-zoom>
<img src="image.jpg" />
</pinch-zoom>
With Configuration (Using Signals)
import { Component, signal } from '@angular/core';
import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom';
@Component({
selector: 'app-example',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom
[transitionDuration]="200"
[doubleTap]="true"
[limitZoom]="3"
[autoZoomOut]="false"
[disabled]="isDisabled()"
(zoomChanged)="onZoomChange($event)"
>
<img src="image.jpg" />
</pinch-zoom>
`,
})
export class ExampleComponent {
isDisabled = signal(false);
onZoomChange(scale: number) {
console.log('Current zoom level:', scale);
}
}
Programmatic Control
import { Component, viewChild } from '@angular/core';
import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom';
@Component({
selector: 'app-controls',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom #pinchZoom>
<img src="image.jpg" />
</pinch-zoom>
<button (click)="zoomIn()">Zoom In</button>
<button (click)="zoomOut()">Zoom Out</button>
<button (click)="reset()">Reset</button>
`,
})
export class ControlsComponent {
pinchZoom = viewChild<PinchZoomComponent>('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:
import { Component } from '@angular/core';
import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom';
@Component({
selector: 'app-click-zoom',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom [enableClickToZoom]="true" [clickToZoomScale]="2.5">
<img src="image.jpg" />
</pinch-zoom>
`,
})
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:
import { Component } from '@angular/core';
import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom';
@Component({
selector: 'app-brightness',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom
[enableBrightnessControl]="true"
[brightnessStep]="0.1"
[minBrightness]="0.1"
[maxBrightness]="2.0"
(brightnessChanged)="onBrightnessChange($event)"
>
<img src="image.jpg" />
</pinch-zoom>
`,
})
export class BrightnessComponent {
onBrightnessChange(brightness: number) {
console.log('Current brightness:', brightness);
}
}
How the brightness button works:
- Click repeatedly to increase brightness by
brightnessStepincrements - 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:
import { Component, viewChild } from '@angular/core';
import { PinchZoomComponent } from '@brianpooe/ngx-pinch-zoom';
@Component({
selector: 'app-brightness-controls',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom #pinchZoom>
<img src="image.jpg" />
</pinch-zoom>
<button (click)="brightnessIn()">Brighter</button>
<button (click)="brightnessOut()">Darker</button>
<button (click)="resetBrightness()">Reset Brightness</button>
`,
})
export class BrightnessControlsComponent {
pinchZoom = viewChild<PinchZoomComponent>('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:
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: `
<pinch-zoom
#pinchZoom
[disableZoomControl]="'disable'"
[enableBrightnessControl]="false"
[zoomControlScale]="0.5"
[brightnessStep]="0.1"
(zoomChanged)="onZoomChange($event)"
(brightnessChanged)="onBrightnessChange($event)"
>
<img src="image.jpg" />
</pinch-zoom>
<div class="custom-controls">
<button [disabled]="isZoomAtMin()" (click)="handleZoomOut()">Zoom -</button>
<button (click)="handleZoomIn()">Zoom +</button>
<button [disabled]="isBrightnessAtMin()" (click)="handleBrightnessOut()">Brightness -</button>
<button (click)="handleBrightnessIn()">Brightness +</button>
</div>
<p>Zoom: {{ zoomLevel() }} | Brightness: {{ brightnessLevel() }}</p>
`,
})
export class CustomControlsComponent {
pinchZoom = viewChild<PinchZoomComponent>('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<number> |
Emits current scale when zoom changes |
brightnessChanged |
OutputEmitterRef<number> |
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.
// Before (Angular <16)
@Input() disabled: boolean = false;
// Now (Angular 20+)
disabled = input<boolean>(false);
Output Signals
Outputs use the new output() API:
// Before
@Output() zoomChanged = new EventEmitter<number>();
// Now
zoomChanged = output<number>();
Computed Signals
Derived state uses computed signals:
isZoomedIn = computed<boolean>(() => {
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:
- Inputs: No changes needed in templates, binding syntax remains the same
- Outputs: Event binding syntax remains the same
- ViewChild: Update to
viewChildsignal (optional but recommended) - Component properties: Access computed properties by calling them:
component.scale()
Contributing
See 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 - Original creator and core zoom algorithms
- Repository: drozhzhin-n-e/ngx-pinch-zoom
Angular 19/20 Compatibility Fork:
- Contributors:
- Konstantin Schütte (medDV-GmbH) - Angular 19/20 compatibility updates
- Björn Schmidt (medDV-GmbH) - Angular 19/20 compatibility updates
- Repository: medDV-GmbH/ngx-pinch-zoom
Current Version:
- Maintainer: Brian Pooe - Angular 20 signals migration, architecture redesign, new features (brightness control, click-to-zoom), comprehensive documentation
Issues and Support
Please report issues on GitHub Issues