Implement click-to-zoom feature perfect for anomaly detection and defect inspection workflows: **New Input Signals:** - enableClickToZoom: Enable/disable click-to-zoom (default: false) - clickToZoomScale: Target zoom scale when clicking (default: 2.5) **Implementation:** - Click any point on image to zoom to that exact location - Click again to zoom out back to original view - Cursor changes to zoom-in icon when feature is enabled - Smooth animated transitions - Respects zoom limits (min/max scale) - Works alongside existing touch/pinch zoom **Use Cases:** - Anomaly detection: Click suspicious areas to examine closely - Defect inspection: Quickly zoom to specific spots - Detail review: Fast workflow for inspecting multiple points **Technical Details:** - Added IvyPinch.zoomToPoint(clientX, clientY, targetScale) method - Calculates click position relative to element bounds - Applies zoom transform centered on clicked point - Component wrapper method handles event and configuration - CSS cursor feedback for better UX Updated documentation with usage examples and testing scenarios.
17 KiB
Quick Reference Guide
Fast lookup reference for ngx-pinch-zoom maintainers. For detailed explanations, see ARCHITECTURE.md and IMPLEMENTATION_GUIDE.md.
Table of Contents
- Key Concepts
- Code Locations
- Common Patterns
- Signal Architecture
- Debugging Checklist
- Performance Tips
- Common Gotchas
- Testing Scenarios
Key Concepts
The Three Core Classes
| Class | Location | Purpose | Dependencies |
|---|---|---|---|
| PinchZoomComponent | pinch-zoom.component.ts |
Angular component wrapper | Angular Core |
| IvyPinch | ivypinch.ts |
Core zoom/pan logic | None (pure TS) |
| Touches | touches.ts |
Event detection & handling | None (pure TS) |
Data Flow
graph LR
A[User Input]:::userStyle --> B[Touches<br/>detect gesture]:::touchStyle
B --> C[IvyPinch<br/>calculate transform]:::logicStyle
C --> D[DOM Update]:::domStyle
D --> E[Signal Update]:::signalStyle
E --> F[Event Emission]:::eventStyle
classDef userStyle fill:#a371f7,stroke:#d29eff,color:#fff
classDef touchStyle fill:#3fb950,stroke:#56d364,color:#fff
classDef logicStyle fill:#58a6ff,stroke:#79c0ff,color:#fff
classDef domStyle fill:#f85149,stroke:#ff7b72,color:#fff
classDef signalStyle fill:#d29922,stroke:#e3b341,color:#fff
classDef eventStyle fill:#db61a2,stroke:#f778ba,color:#fff
Transform Mathematics
// The core formula applied to elements:
transform: translate3d(${moveX}px, ${moveY}px, 0) scale(${scale})
Code Locations
Where to Find Things
| What | File | Line Range | Purpose |
|---|---|---|---|
| Input signals | pinch-zoom.component.ts |
~47-70 | Component configuration |
| Computed signals | pinch-zoom.component.ts |
~78-120 | Derived state |
| Pinch handling | ivypinch.ts |
~300-400 | Pinch zoom logic |
| Pan handling | ivypinch.ts |
~400-500 | Pan/drag logic |
| Touch detection | touches.ts |
~150-250 | Gesture recognition |
| Transform application | ivypinch.ts |
~600-700 | DOM manipulation |
| Default properties | properties.ts |
All | Configuration defaults |
| Type definitions | interfaces.ts |
All | TypeScript interfaces |
Key Methods by Task
| Task | Method | File | What It Does |
|---|---|---|---|
| User zooms in | handlePinch() |
ivypinch.ts |
Calculates new scale from finger distance |
| User pans | handleLinearSwipe() |
ivypinch.ts |
Calculates new position |
| Apply changes | transformElement() |
ivypinch.ts |
Updates CSS transform |
| Detect pinch | detectPinch() |
touches.ts |
Recognizes 2-finger gesture |
| Detect pan | detectLinearSwipe() |
touches.ts |
Recognizes 1-finger drag |
| Detect double-tap | detectDoubleTap() |
touches.ts |
Recognizes rapid taps |
| Brightness up | brightnessIn() |
pinch-zoom.component.ts |
Increases brightness by step |
| Brightness down | brightnessOut() |
pinch-zoom.component.ts |
Decreases brightness by step |
| Reset brightness | resetBrightness() |
pinch-zoom.component.ts |
Resets brightness to 1.0 |
| Click to zoom | zoomToPoint() |
pinch-zoom.component.ts |
Zooms to clicked point |
Common Patterns
Adding a New Input Property
// 1. Add to component inputs (pinch-zoom.component.ts)
myNewOption = input<boolean>(false);
// 2. Add to mergedProperties computed signal
mergedProperties = computed<ComponentProperties>(() => {
return {
...this.defaultComponentProperties,
myNewOption: this.myNewOption(),
// ... other properties
};
});
// 3. Add to interface (interfaces.ts)
export interface Properties {
myNewOption?: boolean;
// ... other properties
}
// 4. Add default value (properties.ts)
export const defaultProperties: Properties = {
myNewOption: false,
// ... other properties
};
Adding a New Output Event
// 1. Create output signal (pinch-zoom.component.ts)
myNewEvent = output<MyEventType>();
// 2. Emit when appropriate
this.myNewEvent.emit(eventData);
Adding CSS Filter Effects (Brightness Example)
// 1. Add input signals for configuration (pinch-zoom.component.ts)
enableBrightnessControl = input<boolean>(false);
brightnessStep = input<number>(0.1);
// 2. Add internal signal for state
private currentBrightness = signal<number>(1.0);
// 3. Add computed signal for public access
brightness = computed<number>(() => {
return this.currentBrightness();
});
// 4. Add control methods
brightnessIn(): number {
const newBrightness = Math.min(
this.brightness() + this.brightnessStep(),
this.maxBrightness()
);
this.currentBrightness.set(newBrightness);
this.brightnessChanged.emit(newBrightness);
return newBrightness;
}
// 5. Add effect to apply CSS filter (constructor)
effect(() => {
const brightness = this.brightness();
const element = this.elementRef.nativeElement.querySelector('.pinch-zoom-content');
if (element) {
element.style.filter = `brightness(${brightness})`;
}
});
// 6. Add UI controls in template
<!-- In pinch-zoom.component.html -->
@if (isBrightnessControl()) {
<div class="pz-brightness-controls">
<button (click)="brightnessIn()">Brighter</button>
<button (click)="brightnessOut()">Darker</button>
</div>
}
Adding New Gesture Logic
// 1. Add event type (touches.ts)
export type TouchEvent = 'pinch' | 'pan' | 'my-new-gesture';
// 2. Add detection method (touches.ts)
private detectMyNewGesture(): boolean {
// Detection logic
this.eventType = 'my-new-gesture';
return true;
}
// 3. Call from detectGesture() (touches.ts)
detectGesture() {
this.detectMyNewGesture() ||
this.detectPinch() ||
this.detectLinearSwipe() ||
this.detectDoubleTap();
}
// 4. Add handler (ivypinch.ts)
private handleMyNewGesture = (event: any) => {
// Handle gesture
};
// 5. Register handler (ivypinch.ts, in constructor)
this.touches.on('my-new-gesture', this.handleMyNewGesture as any);
Signal Architecture
Input Signals (Configuration from User)
// Zoom controls
transitionDuration = input<number>(200); // Animation speed
doubleTap = input<boolean>(true); // Enable double-tap
limitZoom = input<number>(3); // Max zoom level
disabled = input<boolean>(false); // Disable all
// Brightness controls
enableBrightnessControl = input<boolean>(false); // Enable brightness UI
brightnessStep = input<number>(0.1); // Brightness step size
minBrightness = input<number>(0.1); // Min brightness
maxBrightness = input<number>(2.0); // Max brightness
// Click-to-zoom controls
enableClickToZoom = input<boolean>(false); // Enable click-to-zoom
clickToZoomScale = input<number>(2.5); // Target scale on click
Internal Signals (Component State)
private currentScale = signal<number>(1); // Current zoom
private currentBrightness = signal<number>(1.0); // Current brightness
private isZooming = signal<boolean>(false); // Is user zooming
private isPanning = signal<boolean>(false); // Is user panning
Computed Signals (Derived State)
// Zoom-related
isZoomedIn = computed(() => this.scale() > 1);
maxScale = computed(() => this.calculateMaxScale());
isControl = computed(() => this.shouldShowControls());
// Brightness-related
brightness = computed(() => this.currentBrightness());
isBrightnessControl = computed(() => this.enableBrightnessControl());
isBrightnessAtMin = computed(() => this.brightness() <= this.minBrightness());
isBrightnessAtMax = computed(() => this.brightness() >= this.maxBrightness());
Output Signals (Events to User)
zoomChanged = output<number>(); // Emits when scale changes
brightnessChanged = output<number>(); // Emits when brightness changes
Signal Effects (Side Effects)
constructor() {
effect(() => {
// React to signal changes
if (this.disabled()) {
this.pinchZoom?.destroy();
}
});
}
Debugging Checklist
Image Not Zooming
- ✅ Check
disabledinput isfalse - ✅ Verify element has content (image loaded)
- ✅ Check browser console for errors
- ✅ Verify
limitZoomis not set to1 - ✅ Test with both touch and mouse wheel
- ✅ Check if
disableZoomControlis blocking input
Panning Not Working
- ✅ Verify image is zoomed in (
scale > 1) - ✅ Check
disablePanisfalse - ✅ Verify
scale >= minPanScale(default 1.0001) - ✅ Test that touch/mouse events are firing
- ✅ Check if
limitPanis preventing movement
Transforms Not Applying
- ✅ Inspect element CSS in DevTools
- ✅ Verify
transformElement()is being called - ✅ Check transform string format
- ✅ Look for conflicting CSS rules
- ✅ Verify element reference is valid
Performance Issues
- ✅ Check for excessive logging
- ✅ Verify not triggering change detection loops
- ✅ Look for memory leaks (destroy() called?)
- ✅ Check image size (too large?)
- ✅ Verify using
transform(notleft/top)
Event Handler Issues
- ✅ Verify listeners are registered in constructor
- ✅ Check event types match ('pinch', 'pan', etc.)
- ✅ Ensure handlers use arrow functions or
.bind(this) - ✅ Verify destroy() removes listeners
- ✅ Check
as anytype assertions are correct
Performance Tips
DO Use
✅ CSS Transform - Hardware accelerated
element.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
✅ Signals for State - Fine-grained reactivity
private currentScale = signal<number>(1);
✅ Computed for Derived Values - Automatic memoization
isZoomedIn = computed(() => this.scale() > 1);
✅ translate3d - Triggers GPU acceleration
translate3d(${moveX}px, ${moveY}px, 0) // Force 3D rendering
DON'T Use
❌ Position Properties - Triggers reflow
element.style.left = `${x}px`; // Bad!
element.style.top = `${y}px`; // Bad!
❌ Excessive Logging - Performance cost
console.log(this.scale); // Remove in production
❌ Change Detection Triggers - Causes re-renders
// Avoid calling this repeatedly
this.cdr.markForCheck();
Common Gotchas
1. Touch Events Don't Bubble Correctly
Problem: Parent elements don't receive touch events.
Solution: Event listeners are on the container element directly.
// In IvyPinch constructor
this.element.addEventListener('touchstart', handler);
2. Pinch Scale Jumps on First Touch
Problem: Initial distance is 0, causing division issues.
Solution: Initialize distance in handlePinch:
if (this.distance === 0) {
this.distance = touches.distance;
}
3. Pan Continues After Lifting Finger
Problem: No touchend handler to stop panning.
Solution: Touches class handles this:
handleTouchEnd() {
this.eventType = undefined; // Clears gesture
}
4. Zoom Buttons Don't Show/Hide
Problem: isControl computed signal not updating.
Solution: Check dependencies in computed:
isControl = computed(() => {
const control = this.disableZoomControl();
const zoomed = this.isZoomedIn();
// ... logic depends on these signals
});
5. Memory Leaks on Component Destroy
Problem: Event listeners not removed.
Solution: Call destroy() in ngOnDestroy:
ngOnDestroy(): void {
this.pinchZoom?.destroy();
}
6. Double-Tap Fires Twice
Problem: Both touchend and click fire.
Solution: Touches class uses timeout to distinguish:
private doubleTapTimeout: number = 0;
detectDoubleTap(): boolean {
clearTimeout(this.doubleTapTimeout);
// ... timing logic
}
Gesture State Machine
Touch Event Flow
flowchart TD
A[User touches screen]:::userAction --> B[touchstart event]:::browserEvent
B --> C[Touches.handleTouchStart]:::touchHandler
C --> D[Store initial position]:::stateUpdate
D --> E[User moves fingers]:::userAction
E --> F[touchmove event]:::browserEvent
F --> G[Touches.handleTouchMove]:::touchHandler
G --> H[Touches.detectGesture]:::detection
H --> I{Determines gesture type}:::decision
I -->|pinch| J[Sets eventType = 'pinch']:::stateUpdate
I -->|pan| K[Sets eventType = 'pan']:::stateUpdate
I -->|doubletap| L[Sets eventType = 'tap']:::stateUpdate
J --> M[Emits event via handler]:::emission
K --> M
L --> M
M --> N[IvyPinch.handlePinch/handlePan]:::logicHandler
N --> O[Calculate new scale/position]:::calculation
O --> P[Apply constraints]:::calculation
P --> Q[IvyPinch.transformElement]:::domUpdate
Q --> R[Update element.style.transform]:::domUpdate
R --> S[Call zoomChanged callback]:::callback
S --> T[Component updates signals]:::signalUpdate
T --> U[User lifts fingers]:::userAction
U --> V[touchend event]:::browserEvent
V --> W[Touches.handleTouchEnd]:::touchHandler
W --> X[Reset eventType]:::stateUpdate
X --> Y[Done]:::end
classDef userAction fill:#a371f7,stroke:#d29eff,color:#fff
classDef browserEvent fill:#db61a2,stroke:#f778ba,color:#fff
classDef touchHandler fill:#3fb950,stroke:#56d364,color:#fff
classDef stateUpdate fill:#d29922,stroke:#e3b341,color:#fff
classDef detection fill:#58a6ff,stroke:#79c0ff,color:#fff
classDef decision fill:#f85149,stroke:#ff7b72,color:#fff
classDef emission fill:#bc8cff,stroke:#d2a8ff,color:#fff
classDef logicHandler fill:#58a6ff,stroke:#79c0ff,color:#fff
classDef calculation fill:#58a6ff,stroke:#79c0ff,color:#fff
classDef domUpdate fill:#f85149,stroke:#ff7b72,color:#fff
classDef callback fill:#d29922,stroke:#e3b341,color:#fff
classDef signalUpdate fill:#d29922,stroke:#e3b341,color:#fff
classDef end fill:#3fb950,stroke:#56d364,color:#fff
Testing Scenarios
Manual Testing Checklist
Basic Functionality
- Pinch zoom in with 2 fingers
- Pinch zoom out with 2 fingers
- Pan image when zoomed in
- Double-tap to zoom in
- Double-tap again to zoom out
- Mouse wheel to zoom
- Zoom buttons (if enabled)
- Brightness controls (if enabled)
- Increase brightness button
- Decrease brightness button
- Click-to-zoom (if enabled)
- Click to zoom in to point
- Click again to zoom out
Edge Cases
- Zoom in beyond max limit
- Zoom out beyond min limit
- Pan beyond image edges (with limitPan)
- Rapid zoom in/out
- Change orientation while zoomed
- Multiple rapid double-taps
- Brightness at minimum (controls disabled)
- Brightness at maximum (controls disabled)
- Rapid brightness adjustments
Configuration Tests
- Set
disabled=true→ all gestures disabled - Set
disablePan=true→ cannot pan - Set
doubleTap=false→ no double-tap - Set
limitZoom=2→ stops at 2x - Set
autoZoomOut=true→ resets after pinch - Set
wheel=false→ mouse wheel disabled - Set
enableBrightnessControl=true→ shows brightness controls - Set
brightnessStep=0.2→ larger brightness steps - Set
minBrightness=0.5→ stops at 0.5 minimum - Set
maxBrightness=1.5→ stops at 1.5 maximum - Set
enableClickToZoom=true→ enables click-to-zoom - Set
clickToZoomScale=3.0→ zooms to 3x on click
Performance Tests
- Large image (>5MB) zooms smoothly
- Rapid gestures don't lag
- No memory leaks after destroy
- Smooth animations at 60fps
Quick Command Reference
# Build library
npm run build
# Watch mode
npm run watch
# Format code
npm run format
# Check formatting
npm run format:check
File Size Reference
| File | Lines | Complexity |
|---|---|---|
pinch-zoom.component.ts |
~200 | Medium |
ivypinch.ts |
~800 | High |
touches.ts |
~500 | High |
interfaces.ts |
~30 | Low |
properties.ts |
~40 | Low |
Version Compatibility
| Library Version | Angular Version | TypeScript Version |
|---|---|---|
| 20.0.0 | 20.0.0+ | 5.8.0+ |
Tip: Use your editor's "Go to Definition" (F12) to jump from references to implementations quickly!