From c0546dbf46c87e0b8fb9a383c79913e414888145 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 16:14:34 +0000 Subject: [PATCH] style: Apply prettier formatting Apply consistent code formatting with prettier to all project files --- CHANGELOG.md | 64 +++--- CONTRIBUTING.md | 65 +++--- angular.json | 188 +++++++++--------- docs/ARCHITECTURE.md | 86 ++++---- docs/IMPLEMENTATION_GUIDE.md | 146 +++++++------- docs/README.md | 100 +++++----- .../src/lib/ivypinch.improved.ts | 15 +- projects/ngx-pinch-zoom/src/lib/ivypinch.ts | 26 +-- .../src/lib/pinch-zoom.component.html | 82 ++++---- .../src/lib/pinch-zoom.component.ts | 24 +-- projects/ngx-pinch-zoom/src/lib/touches.ts | 19 +- src/app/app.component.html | 40 ++-- src/app/app.component.ts | 2 +- src/app/app.module.ts | 2 +- src/index.html | 2 +- 15 files changed, 421 insertions(+), 440 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40add7d..a7fdc64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,15 +35,15 @@ This release modernizes the library with Angular 20 and signals API. - **Karma/Jasmine**: Removed old testing infrastructure - **ESLint**: Removed eslint and unnecessary linting dependencies - **Polyfills**: Removed polyfills.ts (not needed in modern browsers) -- **Test Files**: Removed *.spec.ts files (to be reimplemented with modern testing tools) +- **Test Files**: Removed \*.spec.ts files (to be reimplemented with modern testing tools) - **Backward Compatibility Inputs**: Removed deprecated kebab-case input bindings ### 🔧 Technical Changes - Updated `tsconfig.json` with modern settings: - - `moduleResolution: "bundler"` - - `strict: true` - - `strictTemplates: true` + - `moduleResolution: "bundler"` + - `strict: true` + - `strictTemplates: true` - Simplified `angular.json` configuration - Removed deprecated tslint configuration - Updated package scripts for simpler workflow @@ -52,6 +52,7 @@ This release modernizes the library with Angular 20 and signals API. ### 📦 Dependencies #### Updated + - `@angular/*`: 19.0.0 → 20.0.0 - `typescript`: 5.5.4 → 5.8.0 - `rxjs`: 7.5.2 → 7.8.0 @@ -61,6 +62,7 @@ This release modernizes the library with Angular 20 and signals API. - `prettier`: 2.7.1 → 3.0.0 #### Removed + - `cypress` - `@cypress/schematic` - `karma` and related packages @@ -91,13 +93,15 @@ This release modernizes the library with Angular 20 and signals API. 2. **Minimum TypeScript Version**: Now requires TypeScript 5.8.0+ 3. **Minimum Node Version**: Now requires Node.js 18.19.1+ 4. **Component Properties**: Computed properties must be called as functions: - ```typescript - // Before - component.scale - // After - component.scale() - ``` + ```typescript + // Before + component.scale; + + // After + component.scale(); + ``` + 5. **Removed Exports**: Some internal utilities may no longer be exported ### 🔄 Migration Guide @@ -105,34 +109,38 @@ This release modernizes the library with Angular 20 and signals API. For users upgrading from previous versions: 1. Update Angular to 20+: - ```bash - ng update @angular/core@20 @angular/cli@20 - ``` + + ```bash + ng update @angular/core@20 @angular/cli@20 + ``` 2. Update your package.json: - ```bash - npm install @meddv/ngx-pinch-zoom@20.0.0 - ``` + + ```bash + npm install @meddv/ngx-pinch-zoom@20.0.0 + ``` 3. Template changes: None required! Input/output binding syntax remains the same. 4. Component reference changes (if using ViewChild): - ```typescript - // Before - @ViewChild('pinchZoom') pinchZoom: PinchZoomComponent; - // After (recommended) - pinchZoom = viewChild('pinchZoom'); - ``` + ```typescript + // Before + @ViewChild('pinchZoom') pinchZoom: PinchZoomComponent; + + // After (recommended) + pinchZoom = viewChild('pinchZoom'); + ``` 5. Accessing computed properties: - ```typescript - // Before - const scale = this.pinchZoom.scale; - // After - const scale = this.pinchZoom()?.scale(); - ``` + ```typescript + // Before + const scale = this.pinchZoom.scale; + + // After + const scale = this.pinchZoom()?.scale(); + ``` ### 🙏 Credits diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01c0cf6..a9ce0d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,20 +40,20 @@ ngx-pinch-zoom/ ### Core Components 1. **PinchZoomComponent** (`pinch-zoom.component.ts`) - - Main Angular component - - Uses signals for all inputs and outputs - - Implements modern Angular patterns (inject, computed, effect) - - Standalone component + - Main Angular component + - Uses signals for all inputs and outputs + - Implements modern Angular patterns (inject, computed, effect) + - Standalone component 2. **IvyPinch** (`ivypinch.ts`) - - Core zoom/pan logic - - Handles transformations and calculations - - No Angular dependencies (plain TypeScript class) + - Core zoom/pan logic + - Handles transformations and calculations + - No Angular dependencies (plain TypeScript class) 3. **Touches** (`touches.ts`) - - Event handling for touch and mouse interactions - - Gesture detection (pinch, pan, tap, double-tap) - - Cross-browser compatibility + - Event handling for touch and mouse interactions + - Gesture detection (pinch, pan, tap, double-tap) + - Cross-browser compatibility ## Development Setup @@ -94,6 +94,7 @@ npm run build-lib ``` Output is generated in `dist/ngx-pinch-zoom/` with: + - ESM bundles - TypeScript declaration files - package.json for npm publishing @@ -121,7 +122,7 @@ disabled = input(false); zoomChanged = output(); scale = computed(() => { - return this.currentScale(); + return this.currentScale(); }); ``` @@ -147,30 +148,30 @@ The project uses strict TypeScript settings: ```json { - "strict": true, - "strictNullChecks": true, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true + "strict": true, + "strictNullChecks": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true } ``` ### Adding New Features 1. **Update the component** (`pinch-zoom.component.ts`): - - Add input signals for configuration - - Add computed signals for derived state - - Update the component template if needed + - Add input signals for configuration + - Add computed signals for derived state + - Update the component template if needed 2. **Update core logic** (`ivypinch.ts`): - - Add properties to the `Properties` interface - - Implement the functionality - - Test calculations thoroughly + - Add properties to the `Properties` interface + - Implement the functionality + - Test calculations thoroughly 3. **Document the feature**: - - Update README.md with the new property - - Add usage examples - - Document default values + - Update README.md with the new property + - Add usage examples + - Document default values ### Testing Changes @@ -205,16 +206,16 @@ npm update ### Debugging 1. **Component Issues**: - - Check signal reactivity with Angular DevTools - - Use `effect(() => console.log(signalValue()))` + - Check signal reactivity with Angular DevTools + - Use `effect(() => console.log(signalValue()))` 2. **Touch/Mouse Issues**: - - Add console.logs in `touches.ts` event handlers - - Check browser dev tools for event listeners + - Add console.logs in `touches.ts` event handlers + - Check browser dev tools for event listeners 3. **Transform Issues**: - - Inspect `ivypinch.ts` transformElement method - - Check CSS transform values in browser inspector + - Inspect `ivypinch.ts` transformElement method + - Check CSS transform values in browser inspector ## Browser Compatibility @@ -237,12 +238,14 @@ The library supports all modern browsers: ### Build Failures **TypeScript errors**: Ensure strict mode compliance + ```bash # Check TypeScript version npx tsc --version # Should be ~5.8.0 ``` **Dependency conflicts**: Clear cache and reinstall + ```bash rm -rf node_modules package-lock.json npm install diff --git a/angular.json b/angular.json index f19b5c2..fd51093 100644 --- a/angular.json +++ b/angular.json @@ -1,101 +1,101 @@ { - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "cli": { - "analytics": false - }, - "projects": { - "ivypinchApp": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "sass" - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:application", - "options": { - "outputPath": "dist/ivypinchApp", - "index": "src/index.html", - "browser": "src/main.ts", - "tsConfig": "tsconfig.app.json", - "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.sass"], - "scripts": [] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "6kB", - "maximumError": "10kB" - } - ] - }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "configurations": { - "production": { - "buildTarget": "ivypinchApp:build:production" - }, - "development": { - "buildTarget": "ivypinchApp:build:development" - } - }, - "defaultConfiguration": "development" - } - } + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "analytics": false }, - "ngx-pinch-zoom": { - "projectType": "library", - "root": "projects/ngx-pinch-zoom", - "sourceRoot": "projects/ngx-pinch-zoom/src", - "prefix": "lib", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:ng-packagr", - "options": { - "project": "projects/ngx-pinch-zoom/ng-package.json" - }, - "configurations": { - "production": { - "tsConfig": "projects/ngx-pinch-zoom/tsconfig.lib.prod.json" + "projects": { + "ivypinchApp": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "sass" + } }, - "development": { - "tsConfig": "projects/ngx-pinch-zoom/tsconfig.lib.json" + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/ivypinchApp", + "index": "src/index.html", + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.sass"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kB", + "maximumError": "10kB" + } + ] + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "ivypinchApp:build:production" + }, + "development": { + "buildTarget": "ivypinchApp:build:development" + } + }, + "defaultConfiguration": "development" + } + } + }, + "ngx-pinch-zoom": { + "projectType": "library", + "root": "projects/ngx-pinch-zoom", + "sourceRoot": "projects/ngx-pinch-zoom/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/ngx-pinch-zoom/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/ngx-pinch-zoom/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ngx-pinch-zoom/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + } } - }, - "defaultConfiguration": "production" } - } } - } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index da0c5b5..db0cddc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -77,42 +77,44 @@ graph TB ### Design Principles 1. **Separation of Concerns** - - **PinchZoomComponent**: Angular integration, signals, lifecycle - - **IvyPinch**: Pure business logic, no Angular dependencies - - **Touches**: Pure event handling, no Angular dependencies + - **PinchZoomComponent**: Angular integration, signals, lifecycle + - **IvyPinch**: Pure business logic, no Angular dependencies + - **Touches**: Pure event handling, no Angular dependencies 2. **Signal-Based Reactivity** - - All inputs use Angular 20+ input signals - - Computed signals for derived state - - Output signals for events - - Effects for side effects + - All inputs use Angular 20+ input signals + - Computed signals for derived state + - Output signals for events + - Effects for side effects 3. **Framework Independence** - - IvyPinch and Touches can work without Angular - - Could be ported to React, Vue, or vanilla JS - - DOM manipulation is isolated + - IvyPinch and Touches can work without Angular + - Could be ported to React, Vue, or vanilla JS + - DOM manipulation is isolated 4. **Performance First** - - CSS transforms for hardware acceleration - - `translate3d` forces GPU rendering - - No layout thrashing (read/write separation) - - Minimal change detection triggers + - CSS transforms for hardware acceleration + - `translate3d` forces GPU rendering + - No layout thrashing (read/write separation) + - Minimal change detection triggers ## Component Architecture ### PinchZoomComponent (Angular Layer) **Responsibilities:** + - Provide Angular component interface - Manage input/output signals - Initialize and cleanup IvyPinch - Bridge between Angular and core logic **Key Features:** + ```typescript @Component({ selector: 'pinch-zoom, [pinch-zoom]', - standalone: true, // No NgModule needed + standalone: true, // No NgModule needed // ... }) export class PinchZoomComponent { @@ -143,6 +145,7 @@ export class PinchZoomComponent { ### IvyPinch (Core Logic Layer) **Responsibilities:** + - Calculate zoom scale from pinch gestures - Calculate pan position from swipe gestures - Apply constraints (min/max zoom, pan limits) @@ -150,21 +153,22 @@ export class PinchZoomComponent { - Notify component of changes **Key State:** + ```typescript class IvyPinch { // Zoom state - public scale: number = 1; // Current scale (1 = 100%) - private initialScale: number = 1; // Scale at gesture start - private maxScale: number = 3; // Maximum allowed scale + public scale: number = 1; // Current scale (1 = 100%) + private initialScale: number = 1; // Scale at gesture start + private maxScale: number = 3; // Maximum allowed scale // Position state - public moveX: number = 0; // Horizontal offset (px) - public moveY: number = 0; // Vertical offset (px) - private initialMoveX: number = 0; // Position at gesture start + public moveX: number = 0; // Horizontal offset (px) + public moveY: number = 0; // Vertical offset (px) + private initialMoveX: number = 0; // Position at gesture start private initialMoveY: number = 0; // Gesture state - private distance: number = 0; // Distance between fingers + private distance: number = 0; // Distance between fingers private initialDistance: number = 0; // Distance at pinch start } ``` @@ -172,12 +176,14 @@ class IvyPinch { ### Touches (Event Handling Layer) **Responsibilities:** + - Listen to touch/mouse events - Detect gesture types (pinch, pan, double-tap) - Calculate touch positions and distances - Emit typed events to IvyPinch **Event Flow:** + ```typescript class Touches { // Event type state machine @@ -190,14 +196,14 @@ class Touches { private handlers = { pinch: [], pan: [], - tap: [] + tap: [], }; // Core detection loop detectGesture() { - this.detectDoubleTap() || // Check tap first - this.detectPinch() || // Then pinch - this.detectLinearSwipe(); // Finally pan + this.detectDoubleTap() || // Check tap first + this.detectPinch() || // Then pinch + this.detectLinearSwipe(); // Finally pan } } ``` @@ -281,6 +287,7 @@ flowchart TD ### Why Signals? Angular 20's signals provide: + 1. **Fine-grained reactivity** - Only affected components update 2. **Automatic dependency tracking** - Computed signals track dependencies 3. **Better performance** - Less change detection overhead @@ -393,6 +400,7 @@ mergedProperties = computed(() => { ``` This ensures: + - Individual inputs take priority - properties object can set multiple at once - Defaults are always present @@ -609,18 +617,18 @@ emit(eventName: string, event: any) { ```typescript type TouchEvent = - | 'touchstart' // Finger touches screen - | 'touchmove' // Finger moves - | 'touchend' // Finger lifts - | 'mousedown' // Mouse button pressed - | 'mousemove' // Mouse moves - | 'mouseup' // Mouse button released - | 'wheel'; // Mouse wheel scrolled + | 'touchstart' // Finger touches screen + | 'touchmove' // Finger moves + | 'touchend' // Finger lifts + | 'mousedown' // Mouse button pressed + | 'mousemove' // Mouse moves + | 'mouseup' // Mouse button released + | 'wheel'; // Mouse wheel scrolled type GestureEvent = - | 'pinch' // Two-finger zoom - | 'pan' // One-finger drag - | 'tap'; // Double-tap + | 'pinch' // Two-finger zoom + | 'pan' // One-finger drag + | 'tap'; // Double-tap ``` ## Transform Mathematics @@ -668,6 +676,7 @@ const newMoveY = this.moveY - (centerY - this.moveY) * (newScale / this.scale - #### Mathematical Derivation Let: + - `P` = point to zoom at (in screen coordinates) - `S₁` = old scale - `S₂` = new scale @@ -677,7 +686,7 @@ Let: We want: Point P maps to same position before and after zoom. Before zoom: `P_image = (P - T₁) / S₁` -After zoom: `P_image = (P - T₂) / S₂` +After zoom: `P_image = (P - T₂) / S₂` Setting equal: `(P - T₁) / S₁ = (P - T₂) / S₂` @@ -699,10 +708,12 @@ transform: translate(100px, 50px) scale(2); ``` Is equivalent to: + 1. Scale by 2 2. Then translate by (100px, 50px) **Our transform:** + ```css transform: translate3d(moveX, moveY, 0) scale(scale); ``` @@ -721,6 +732,7 @@ transform: translate3d(100px, 50px, 0) scale(2); ``` Becomes: + ``` | 2 0 0 100 | | 0 2 0 50 | diff --git a/docs/IMPLEMENTATION_GUIDE.md b/docs/IMPLEMENTATION_GUIDE.md index 998fc91..e095d12 100644 --- a/docs/IMPLEMENTATION_GUIDE.md +++ b/docs/IMPLEMENTATION_GUIDE.md @@ -20,15 +20,17 @@ Let's add the ability to rotate images with a two-finger twist gesture. #### Step 1: Add Properties **File: `interfaces.ts`** + ```typescript export interface Properties { // ... existing properties enableRotation?: boolean; - rotationLockThreshold?: number; // Degrees before rotation activates + rotationLockThreshold?: number; // Degrees before rotation activates } ``` **File: `properties.ts`** + ```typescript export const defaultProperties: Properties = { // ... existing defaults @@ -40,6 +42,7 @@ export const defaultProperties: Properties = { #### Step 2: Add Input Signal **File: `pinch-zoom.component.ts`** + ```typescript export class PinchZoomComponent implements OnInit, OnDestroy { // ... existing inputs @@ -60,14 +63,15 @@ export class PinchZoomComponent implements OnInit, OnDestroy { #### Step 3: Add State to IvyPinch **File: `ivypinch.ts`** + ```typescript export class IvyPinch { // ... existing properties // Rotation state - public rotation: number = 0; // Current rotation in degrees - private initialRotation: number = 0; // Rotation at gesture start - private rotationAngle: number = 0; // Cumulative rotation + public rotation: number = 0; // Current rotation in degrees + private initialRotation: number = 0; // Rotation at gesture start + private rotationAngle: number = 0; // Cumulative rotation // ... existing methods @@ -86,10 +90,7 @@ export class IvyPinch { const touch2 = touches[1]; // Calculate angle between fingers - const angle = Math.atan2( - touch2.clientY - touch1.clientY, - touch2.clientX - touch1.clientX - ) * (180 / Math.PI); + const angle = Math.atan2(touch2.clientY - touch1.clientY, touch2.clientX - touch1.clientX) * (180 / Math.PI); // Initialize on first move if (this.rotationAngle === 0) { @@ -114,20 +115,10 @@ export class IvyPinch { this.rotation = this.initialRotation + delta; // Apply transform with rotation - this.transformElementWithRotation( - this.scale, - this.moveX, - this.moveY, - this.rotation - ); + this.transformElementWithRotation(this.scale, this.moveX, this.moveY, this.rotation); }; - private transformElementWithRotation( - scale: number, - moveX: number, - moveY: number, - rotation: number - ): void { + private transformElementWithRotation(scale: number, moveX: number, moveY: number, rotation: number): void { const transform = ` translate3d(${moveX}px, ${moveY}px, 0) scale(${scale}) @@ -146,6 +137,7 @@ export class IvyPinch { #### Step 4: Register Event Handler **File: `ivypinch.ts` (in constructor)** + ```typescript constructor(properties: Properties, private zoomChanged: (scale: number) => void) { // ... existing initialization @@ -166,6 +158,7 @@ constructor(properties: Properties, private zoomChanged: (scale: number) => void #### Step 5: Add Output Event (Optional) **File: `pinch-zoom.component.ts`** + ```typescript export class PinchZoomComponent implements OnInit, OnDestroy { // ... existing outputs @@ -186,6 +179,7 @@ export class PinchZoomComponent implements OnInit, OnDestroy { #### Step 6: Update IvyPinch Callback **File: `ivypinch.ts`** + ```typescript private zoomChanged: (scale: number, rotation?: number) => void; @@ -200,10 +194,7 @@ private handleRotation = (event: any): void => { #### Step 7: Test ```html - + ``` @@ -215,20 +206,22 @@ Add custom easing functions for smoother zoom animations. #### Step 1: Define Easing Functions **File: `ivypinch.ts`** + ```typescript type EasingFunction = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'; const easingFunctions = { - 'linear': (t: number) => t, + linear: (t: number) => t, 'ease-in': (t: number) => t * t, 'ease-out': (t: number) => t * (2 - t), - 'ease-in-out': (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, + 'ease-in-out': (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), }; ``` #### Step 2: Add Property **File: `interfaces.ts`** + ```typescript export interface Properties { // ... existing @@ -239,6 +232,7 @@ export interface Properties { #### Step 3: Implement Animated Zoom **File: `ivypinch.ts`** + ```typescript private animateZoom( targetScale: number, @@ -300,6 +294,7 @@ handleTouchStart(event: TouchEvent) { #### Step 2: Fix Initialization **File: `ivypinch.ts`** + ```typescript private handleLinearSwipe = (event: any): void => { // Store initial position on first move @@ -345,6 +340,7 @@ private resetSwipeState(): void { #### Fix 1: Adjust Timing Window **File: `touches.ts`** + ```typescript detectDoubleTap(): boolean { const now = Date.now(); @@ -425,6 +421,7 @@ detectDoubleTap(): boolean { #### Step 1: Verify Listeners Are Removed **File: `touches.ts`** + ```typescript destroy(): void { console.log('[Touches] Removing listeners'); @@ -452,6 +449,7 @@ destroy(): void { #### Step 2: Ensure destroy() is Called **File: `pinch-zoom.component.ts`** + ```typescript ngOnDestroy(): void { console.log('[PinchZoom] Component destroying'); @@ -482,6 +480,7 @@ ngOnDestroy(): void { Zoom to a specific coordinate programmatically. **File: `ivypinch.ts`** + ```typescript public zoomToPoint( targetScale: number, @@ -555,6 +554,7 @@ private animateZoomToPoint( ``` **Usage:** + ```typescript // In component @ViewChild('pinchZoom') pinchZoomRef!: PinchZoomComponent; @@ -570,6 +570,7 @@ zoomToTopLeft() { Add a visual indicator of current zoom level. **File: `pinch-zoom.component.ts`** + ```typescript export class PinchZoomComponent implements OnInit, OnDestroy { // ... existing code @@ -590,23 +591,25 @@ export class PinchZoomComponent implements OnInit, OnDestroy { ``` **File: `pinch-zoom.component.html`** + ```html
@if (isZoomedIn()) { -
-
{{ zoomPercentage() }}%
-
-
-
+
+
{{ zoomPercentage() }}%
+
+
+
}
``` **File: `pinch-zoom.component.sass`** + ```sass .zoom-indicator position: absolute @@ -643,6 +646,7 @@ export class PinchZoomComponent implements OnInit, OnDestroy { Reduce the number of transform updates during panning. **File: `ivypinch.ts`** + ```typescript private lastPanTime: number = 0; private readonly PAN_THROTTLE_MS = 16; // ~60fps @@ -664,6 +668,7 @@ private handleLinearSwipe = (event: any): void => { Batch transform updates to animation frames. **File: `ivypinch.ts`** + ```typescript private rafId: number | null = null; private pendingTransform: { @@ -712,6 +717,7 @@ public destroy(): void { Avoid expensive calculations on every resize event. **File: `ivypinch.ts`** + ```typescript private resizeTimeout: number | null = null; @@ -747,6 +753,7 @@ public destroy(): void { Add visual overlay showing current transform state. **Create: `transform-debugger.component.ts`** + ```typescript import { Component, input, computed } from '@angular/core'; @@ -763,37 +770,39 @@ import { Component, input, computed } from '@angular/core';
`, - styles: [` - .debug-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - z-index: 9999; - } + styles: [ + ` + .debug-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 9999; + } - .debug-info { - position: absolute; - top: 10px; - right: 10px; - background: rgba(0, 0, 0, 0.8); - color: #0f0; - padding: 10px; - font-family: monospace; - font-size: 12px; - } + .debug-info { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: #0f0; + padding: 10px; + font-family: monospace; + font-size: 12px; + } - .debug-grid { - width: 100%; - height: 100%; - background-image: - linear-gradient(rgba(0, 255, 0, 0.1) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 255, 0, 0.1) 1px, transparent 1px); - background-size: 50px 50px; - } - `] + .debug-grid { + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(0, 255, 0, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 255, 0, 0.1) 1px, transparent 1px); + background-size: 50px 50px; + } + `, + ], }) export class TransformDebugger { scale = input(1); @@ -805,16 +814,13 @@ export class TransformDebugger { ``` **Usage:** + ```html - - + ``` ### Technique 2: Event Flow Logging @@ -822,6 +828,7 @@ export class TransformDebugger { Add comprehensive logging to trace event flow. **File: `touches.ts`** + ```typescript private logEvent(stage: string, data: any): void { if (!this.debug) return; @@ -853,6 +860,7 @@ handleTouchMove(event: TouchEvent): void { ### Unit Testing IvyPinch **File: `ivypinch.spec.ts`** + ```typescript import { IvyPinch } from './ivypinch'; import { Properties } from './interfaces'; @@ -892,8 +900,8 @@ describe('IvyPinch', () => { const mockEvent = { touches: [ { clientX: 0, clientY: 0 }, - { clientX: 500, clientY: 0 } - ] + { clientX: 500, clientY: 0 }, + ], }; // This would zoom to 5x, but limit is 3x @@ -914,6 +922,7 @@ describe('IvyPinch', () => { ### Integration Testing **File: `pinch-zoom.component.spec.ts`** + ```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PinchZoomComponent } from './pinch-zoom.component'; @@ -924,7 +933,7 @@ describe('PinchZoomComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PinchZoomComponent] + imports: [PinchZoomComponent], }).compileComponents(); fixture = TestBed.createComponent(PinchZoomComponent); @@ -964,6 +973,7 @@ describe('PinchZoomComponent', () => { ## Summary This guide covers: + - ✅ Adding new features (rotation, easing) - ✅ Fixing common bugs (jumps, double-tap, memory leaks) - ✅ Common customizations (zoom to point, indicators) diff --git a/docs/README.md b/docs/README.md index b9ab9e9..6741e1b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,25 +7,25 @@ Welcome! This documentation will help you understand and maintain the ngx-pinch- ### For New Maintainers - Start Here 1. **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** ⚡ - - Fast lookup for common tasks - - Code locations - - Common patterns - - Debugging checklist - - **Read this first for quick answers!** + - Fast lookup for common tasks + - Code locations + - Common patterns + - Debugging checklist + - **Read this first for quick answers!** 2. **[ARCHITECTURE.md](ARCHITECTURE.md)** 🏗️ - - Deep dive into how everything works - - Component interaction diagrams - - Transform mathematics explained - - Event flow documentation - - **Read this to understand the system** + - Deep dive into how everything works + - Component interaction diagrams + - Transform mathematics explained + - Event flow documentation + - **Read this to understand the system** 3. **[IMPLEMENTATION_GUIDE.md](IMPLEMENTATION_GUIDE.md)** 🛠️ - - Step-by-step implementation examples - - How to add features - - How to fix bugs - - Code customizations - - **Read this when making changes** + - Step-by-step implementation examples + - How to add features + - How to fix bugs + - Code customizations + - **Read this when making changes** ### Documentation Organization @@ -62,50 +62,50 @@ docs/ ### "Where do I find...?" -| Question | Answer | -|----------|--------| -| How pinch zoom works? | ARCHITECTURE.md → "Transform Mathematics" | -| How to add an input property? | IMPLEMENTATION_GUIDE.md → "Adding New Features" | -| Why isn't pan working? | QUICK_REFERENCE.md → "Debugging Checklist" | -| What is `transformElement()`? | ARCHITECTURE.md → "Transform Mathematics" | -| How to add rotation? | IMPLEMENTATION_GUIDE.md → "Feature: Add Rotation Support" | -| Performance best practices? | QUICK_REFERENCE.md → "Performance Tips" | -| Event flow diagram? | ARCHITECTURE.md → "Event System" | +| Question | Answer | +| ----------------------------- | --------------------------------------------------------- | +| How pinch zoom works? | ARCHITECTURE.md → "Transform Mathematics" | +| How to add an input property? | IMPLEMENTATION_GUIDE.md → "Adding New Features" | +| Why isn't pan working? | QUICK_REFERENCE.md → "Debugging Checklist" | +| What is `transformElement()`? | ARCHITECTURE.md → "Transform Mathematics" | +| How to add rotation? | IMPLEMENTATION_GUIDE.md → "Feature: Add Rotation Support" | +| Performance best practices? | QUICK_REFERENCE.md → "Performance Tips" | +| Event flow diagram? | ARCHITECTURE.md → "Event System" | ### "How do I...?" -| Task | Document | -|------|----------| -| Fix image jumping | IMPLEMENTATION_GUIDE.md → "Bug: Image Jumps on First Touch" | +| Task | Document | +| ------------------------ | ----------------------------------------------------------------------- | +| Fix image jumping | IMPLEMENTATION_GUIDE.md → "Bug: Image Jumps on First Touch" | | Add custom zoom behavior | IMPLEMENTATION_GUIDE.md → "Custom: Programmatic Zoom to Specific Point" | -| Debug transforms | IMPLEMENTATION_GUIDE.md → "Technique 1: Visual Transform Debugging" | -| Improve performance | IMPLEMENTATION_GUIDE.md → "Performance Optimization" | -| Add zoom indicator | IMPLEMENTATION_GUIDE.md → "Custom: Zoom Level Indicator" | +| Debug transforms | IMPLEMENTATION_GUIDE.md → "Technique 1: Visual Transform Debugging" | +| Improve performance | IMPLEMENTATION_GUIDE.md → "Performance Optimization" | +| Add zoom indicator | IMPLEMENTATION_GUIDE.md → "Custom: Zoom Level Indicator" | ## 📖 Reading Code ### Source Files (In Order of Complexity) 1. **properties.ts** (Easiest) - - Just default configuration values - - Good starting point + - Just default configuration values + - Good starting point 2. **interfaces.ts** (Easy) - - TypeScript type definitions - - Understand data structures + - TypeScript type definitions + - Understand data structures 3. **pinch-zoom.component.ts** (Medium) - - Angular component with signals - - See how properties flow + - Angular component with signals + - See how properties flow 4. **touches.ts** (Medium-Hard) - - Event detection logic - - Gesture recognition + - Event detection logic + - Gesture recognition 5. **ivypinch.ts** (Hardest) - - Core zoom/pan mathematics - - Transform calculations - - **Most complex file in the library** + - Core zoom/pan mathematics + - Transform calculations + - **Most complex file in the library** ### Recommended Reading Order @@ -157,20 +157,20 @@ flowchart TD ### Key Concepts to Understand 1. **CSS Transform Matrix** - - How we apply zoom and pan using a single CSS property - - See: ARCHITECTURE.md → "Transform Mathematics" + - How we apply zoom and pan using a single CSS property + - See: ARCHITECTURE.md → "Transform Mathematics" 2. **Signal-Based Reactivity** - - How Angular signals provide fine-grained updates - - See: ARCHITECTURE.md → "Signal Architecture" + - How Angular signals provide fine-grained updates + - See: ARCHITECTURE.md → "Signal Architecture" 3. **Gesture State Machine** - - How events transition between states - - See: QUICK_REFERENCE.md → "Gesture State Machine" + - How events transition between states + - See: QUICK_REFERENCE.md → "Gesture State Machine" 4. **Coordinate Systems** - - Element-relative vs page-relative positions - - See: ARCHITECTURE.md → "Transform Mathematics" + - Element-relative vs page-relative positions + - See: ARCHITECTURE.md → "Transform Mathematics" ## 🛠️ Making Changes @@ -205,7 +205,7 @@ console.log('[IvyPinch] handlePinch', { scale: this.scale, distance: this.distance, moveX: this.moveX, - moveY: this.moveY + moveY: this.moveY, }); ``` diff --git a/projects/ngx-pinch-zoom/src/lib/ivypinch.improved.ts b/projects/ngx-pinch-zoom/src/lib/ivypinch.improved.ts index 2e229fb..48db272 100644 --- a/projects/ngx-pinch-zoom/src/lib/ivypinch.improved.ts +++ b/projects/ngx-pinch-zoom/src/lib/ivypinch.improved.ts @@ -1003,9 +1003,7 @@ export class IvyPinch { * @returns Distance in pixels */ private getDistance(touches: TouchList): number { - return Math.sqrt( - Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2), - ); + return Math.sqrt(Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2)); } // ======================================================================== @@ -1132,11 +1130,9 @@ export class IvyPinch { // Calculate position to zoom at tap location this.moveX = - this.initialMoveX - - (changedTouches[0].clientX - this.elementPosition.left) * (this.properties.doubleTapScale - 1); + this.initialMoveX - (changedTouches[0].clientX - this.elementPosition.left) * (this.properties.doubleTapScale - 1); this.moveY = - this.initialMoveY - - (changedTouches[0].clientY - this.elementPosition.top) * (this.properties.doubleTapScale - 1); + this.initialMoveY - (changedTouches[0].clientY - this.elementPosition.top) * (this.properties.doubleTapScale - 1); } // Case 2: Programmatic zoom (zoom at center) else { @@ -1279,10 +1275,7 @@ export class IvyPinch { // Check if scaled image exceeds container if (this.scale > 1) { - return ( - imgHeight * this.scale > this.parentElement.offsetHeight || - imgWidth * this.scale > this.parentElement.offsetWidth - ); + return imgHeight * this.scale > this.parentElement.offsetHeight || imgWidth * this.scale > this.parentElement.offsetWidth; } // Check if normal image exceeds container diff --git a/projects/ngx-pinch-zoom/src/lib/ivypinch.ts b/projects/ngx-pinch-zoom/src/lib/ivypinch.ts index f2dba68..7dbdd48 100644 --- a/projects/ngx-pinch-zoom/src/lib/ivypinch.ts +++ b/projects/ngx-pinch-zoom/src/lib/ivypinch.ts @@ -302,7 +302,6 @@ export class IvyPinch { this.scale = newScale; this.zoomChanged(this.scale); - /* Get cursor position over image */ const xCenter = event.clientX - this.elementPosition.left - this.initialMoveX; const yCenter = event.clientY - this.elementPosition.top - this.initialMoveY; @@ -538,9 +537,7 @@ export class IvyPinch { } private getDistance(touches: TouchList): number { - return Math.sqrt( - Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2), - ); + return Math.sqrt(Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2)); } private getImageHeight(): number { @@ -556,15 +553,7 @@ export class IvyPinch { private transformElement(duration: number): void { this.element.style.transition = 'all ' + duration + 'ms'; this.element.style.transform = - 'matrix(' + - Number(this.scale) + - ', 0, 0, ' + - Number(this.scale) + - ', ' + - Number(this.moveX) + - ', ' + - Number(this.moveY) + - ')'; + 'matrix(' + Number(this.scale) + ', 0, 0, ' + Number(this.scale) + ', ' + Number(this.moveX) + ', ' + Number(this.moveY) + ')'; } private isTouchScreen(): boolean { @@ -593,10 +582,7 @@ export class IvyPinch { const imgWidth = this.getImageWidth(); if (this.scale > 1) { - return ( - imgHeight * this.scale > this.parentElement.offsetHeight || - imgWidth * this.scale > this.parentElement.offsetWidth - ); + return imgHeight * this.scale > this.parentElement.offsetHeight || imgWidth * this.scale > this.parentElement.offsetWidth; } if (this.scale === 1) { return imgHeight > this.parentElement.offsetHeight || imgWidth > this.parentElement.offsetWidth; @@ -660,11 +646,9 @@ export class IvyPinch { this.scale = this.initialScale * this.properties.doubleTapScale!; this.zoomChanged(this.scale); this.moveX = - this.initialMoveX - - (changedTouches[0].clientX - this.elementPosition.left) * (this.properties.doubleTapScale - 1); + this.initialMoveX - (changedTouches[0].clientX - this.elementPosition.left) * (this.properties.doubleTapScale - 1); this.moveY = - this.initialMoveY - - (changedTouches[0].clientY - this.elementPosition.top) * (this.properties.doubleTapScale - 1); + this.initialMoveY - (changedTouches[0].clientY - this.elementPosition.top) * (this.properties.doubleTapScale - 1); } else { const zoomControlScale = this.properties.zoomControlScale || 0; this.scale = this.initialScale * (zoomControlScale + 1); diff --git a/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.html b/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.html index ae08199..152ff88 100644 --- a/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.html +++ b/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.html @@ -1,48 +1,48 @@
- +
@if (isControl() || isBrightnessControl()) { -
- - @if (isControl()) { -
- } +
+ + @if (isControl()) { +
+ } - - @if (isBrightnessControl()) { -
-
-
-
- } -
+ + @if (isBrightnessControl()) { +
+
+
+
+ } +
} diff --git a/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.ts b/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.ts index d998846..eeaf326 100644 --- a/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.ts +++ b/projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.ts @@ -1,16 +1,4 @@ -import { - Component, - ElementRef, - HostBinding, - OnInit, - OnDestroy, - computed, - input, - output, - signal, - effect, - inject, -} from '@angular/core'; +import { Component, ElementRef, HostBinding, OnInit, OnDestroy, computed, input, output, signal, effect, inject } from '@angular/core'; import { Properties } from './interfaces'; import { defaultProperties } from './properties'; @@ -269,20 +257,14 @@ export class PinchZoomComponent implements OnInit, OnDestroy { } brightnessIn(): number { - const newBrightness = Math.min( - this.brightness() + this.brightnessStep(), - this.maxBrightness() - ); + const newBrightness = Math.min(this.brightness() + this.brightnessStep(), this.maxBrightness()); this.currentBrightness.set(newBrightness); this.brightnessChanged.emit(newBrightness); return newBrightness; } brightnessOut(): number { - const newBrightness = Math.max( - this.brightness() - this.brightnessStep(), - this.minBrightness() - ); + const newBrightness = Math.max(this.brightness() - this.brightnessStep(), this.minBrightness()); this.currentBrightness.set(newBrightness); this.brightnessChanged.emit(newBrightness); return newBrightness; diff --git a/projects/ngx-pinch-zoom/src/lib/touches.ts b/projects/ngx-pinch-zoom/src/lib/touches.ts index 9b91ba2..ee7264d 100644 --- a/projects/ngx-pinch-zoom/src/lib/touches.ts +++ b/projects/ngx-pinch-zoom/src/lib/touches.ts @@ -24,7 +24,7 @@ export type EventType = | 'wheel' | 'double-tap' | 'resize'; -export type TouchHandler ='handleTouchstart' | 'handleTouchmove' | 'handleTouchend'; +export type TouchHandler = 'handleTouchstart' | 'handleTouchmove' | 'handleTouchend'; export type MouseHandler = 'handleMousedown' | 'handleMousemove' | 'handleMouseup' | 'handleWheel'; export type OtherHandler = 'handleResize'; @@ -124,10 +124,7 @@ export class Touches { } // Determine target element (Window, Document, or this.element) - const target = - listener === 'resize' ? window : - (listener === 'mouseup' || listener === 'mousemove') ? document : - this.element; + const target = listener === 'resize' ? window : listener === 'mouseup' || listener === 'mousemove' ? document : this.element; // Add or remove the listener if (action === 'addEventListener') { @@ -280,11 +277,7 @@ export class Touches { } // Linear swipe - if ( - this.detectLinearSwipe(event) || - this.eventType === 'horizontal-swipe' || - this.eventType === 'vertical-swipe' - ) { + if (this.detectLinearSwipe(event) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { this.handleLinearSwipe(event); } }; @@ -378,11 +371,7 @@ export class Touches { const touches = (event as TouchEvent).touches; if (touches) { - if ( - (touches.length === 1 && !this.eventType) || - this.eventType === 'horizontal-swipe' || - this.eventType === 'vertical-swipe' - ) { + if ((touches.length === 1 && !this.eventType) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { return this.getLinearSwipeType(event); } } else { diff --git a/src/app/app.component.html b/src/app/app.component.html index cf40c05..3f54e55 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -5,7 +5,7 @@

transition-duration: 1000

Defines the speed of the animation of positioning and transforming.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

limit-zoom: 2

Limit the maximum available scale. By default, the maximum scale is calculated based on the original image size.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

minScale: 1

Limit the minimum acceptable scale. With a value of 1, it is recommended to use this parameter with limitPan.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

auto-zoom-out: true

Automatic restoration of the original size of an image after its zooming in by two fingers.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

double-tap: false

Zooming in and zooming out of an image, depending on its current condition, with double tap.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

double-tap-scale: 4

Double tap scaling factor.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

disabled: true

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

disablePan: true

Turn off panning with one finger.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

minPanScale: 2

Minimum zoom at which panning is enabled.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

zoomControlScale: 2

Zoom factor when using zoom controls.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

backgroundColor: 'rgba(0,0,0,0.65)'

The background color of the container.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

limitPan: true

Stop panning when the edge of the image reaches the edge of the screen.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

wheel: false

Scale with the mouse wheel.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

wheelZoomFactor: 0.5

Zoom factor when zoomed in with the mouse wheel.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

tag. When setting the property value to `true`, a subscription to the window resize listener will be created.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

draggableImage: true

Sets the attribute draggable to the img tag.

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

draggableOnPinch: true

-

Zoom State: {{this.zoomstate}}

+

Zoom State: {{ this.zoomstate }}

+