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
559 lines
20 KiB
Markdown
559 lines
20 KiB
Markdown
# ngx-pinch-zoom
|
|
|
|
[](https://angular.io/)
|
|
[](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: `
|
|
<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`:
|
|
|
|
```html
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Usage
|
|
|
|
```html
|
|
<pinch-zoom>
|
|
<img src="image.jpg" />
|
|
</pinch-zoom>
|
|
```
|
|
|
|
### 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: `
|
|
<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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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 `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: `
|
|
<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:
|
|
|
|
```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: `
|
|
<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.
|
|
|
|
```typescript
|
|
// Before (Angular <16)
|
|
@Input() disabled: boolean = false;
|
|
|
|
// Now (Angular 20+)
|
|
disabled = input<boolean>(false);
|
|
```
|
|
|
|
### Output Signals
|
|
|
|
Outputs use the new output() API:
|
|
|
|
```typescript
|
|
// Before
|
|
@Output() zoomChanged = new EventEmitter<number>();
|
|
|
|
// Now
|
|
zoomChanged = output<number>();
|
|
```
|
|
|
|
### Computed Signals
|
|
|
|
Derived state uses computed signals:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
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)
|