功能拆開的時候都很快樂,合併的時候才知道合不合適
就算有先規劃順序,但還是有種瞎子摸象的感覺
操作環境跟程式碼 Day7
import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, Input, OnDestroy } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'app-zoomable-svg',
template: `
<div class="svg-container" #container>
<img #svgImage [src]="imageSrc" [style.transform]="transform" (load)="onImageLoad()">
</div>
<div class="controls">
<button (click)="zoomIn()">放大</button>
<button (click)="zoomOut()">縮小</button>
<button (click)="reset()">重置</button>
<button (click)="rotateClockwise()">順時針旋轉90°</button>
<button (click)="rotateCounterclockwise()">逆時針旋轉90°</button>
</div>
`,
styles: [`
.svg-container {
width: 100%;
height: 75vh;
border: 1px solid #ccc;
overflow: hidden;
position: relative;
}
img {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center center;
}
.controls {
margin-top: 10px;
}
`]
})
export class ZoomableSvgComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() imageSrc: string = '';
@ViewChild('svgImage') svgImage!: ElementRef<HTMLImageElement>;
@ViewChild('container') container!: ElementRef<HTMLDivElement>;
private zoom = 1;
private panX = 0;
private panY = 0;
private rotation = 0;
private imageNaturalWidth = 0;
private imageNaturalHeight = 0;
private resizeSubscription?: Subscription;
// 新增:存儲初始狀態
private initialZoom = 1;
private initialPanX = 0;
private initialPanY = 0;
get transform(): string {
return `translate(-50%, -50%) translate(${this.panX}px, ${this.panY}px) scale(${this.zoom}) rotate(${this.rotation}deg)`;
}
constructor() { }
ngOnInit(): void { }
ngAfterViewInit(): void {
this.setupZoom();
this.setupResizeListener();
}
ngOnDestroy(): void {
this.resizeSubscription?.unsubscribe();
}
private setupZoom(): void {
const element = this.container.nativeElement;
let isDragging = false;
let startX = 0;
let startY = 0;
fromEvent<MouseEvent>(element, 'mousedown').subscribe((e) => {
isDragging = true;
startX = e.clientX - this.panX;
startY = e.clientY - this.panY;
});
fromEvent<MouseEvent>(element, 'mousemove').subscribe((e) => {
if (!isDragging) return;
this.panX = e.clientX - startX;
this.panY = e.clientY - startY;
this.updateTransform();
});
fromEvent<MouseEvent>(element, 'mouseup').subscribe(() => {
isDragging = false;
});
fromEvent<MouseEvent>(element, 'mouseleave').subscribe(() => {
isDragging = false;
});
fromEvent<WheelEvent>(element, 'wheel').subscribe((e) => {
e.preventDefault();
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
const delta = e.deltaY > 0 ? 0.9 : 1.1;
this.zoomTo(x, y, this.zoom * delta);
});
}
private setupResizeListener(): void {
this.resizeSubscription = fromEvent(window, 'resize')
.pipe(debounceTime(200))
.subscribe(() => this.fitImageToContainer());
}
private updateTransform(): void {
this.svgImage.nativeElement.style.transform = this.transform;
}
private zoomTo(x: number, y: number, newZoom: number): void {
const oldZoom = this.zoom;
this.zoom = Math.max(0.1, Math.min(10, newZoom)); // 限制縮放範圍
this.panX -= x * (this.zoom / oldZoom - 1);
this.panY -= y * (this.zoom / oldZoom - 1);
this.updateTransform();
}
zoomIn(): void {
this.zoomTo(0, 0, this.zoom * 1.2);
}
zoomOut(): void {
this.zoomTo(0, 0, this.zoom / 1.2);
}
// 修改:重置方法
reset(): void {
this.zoom = this.initialZoom;
this.panX = this.initialPanX;
this.panY = this.initialPanY;
this.rotation = 0;
this.updateTransform();
}
rotateClockwise(): void {
this.rotation = (this.rotation + 90) % 360;
this.adjustAfterRotation();
}
rotateCounterclockwise(): void {
this.rotation = (this.rotation - 90 + 360) % 360;
this.adjustAfterRotation();
}
private adjustAfterRotation(): void {
const containerRect = this.container.nativeElement.getBoundingClientRect();
const imgRect = this.svgImage.nativeElement.getBoundingClientRect();
// 計算旋轉後的圖片尺寸
const rotatedWidth = this.rotation % 180 === 0 ? imgRect.width : imgRect.height;
const rotatedHeight = this.rotation % 180 === 0 ? imgRect.height : imgRect.width;
// 檢查圖片是否超出容器範圍
if (rotatedWidth > containerRect.width || rotatedHeight > containerRect.height) {
// 計算需要的縮放比例
const scaleX = containerRect.width / rotatedWidth;
const scaleY = containerRect.height / rotatedHeight;
const newZoom = this.zoom * Math.min(scaleX, scaleY);
// 應用新的縮放比例,但保持中心點不變
this.zoomTo(0, 0, newZoom);
} else {
// 如果沒有超出範圍,只需更新變換
this.updateTransform();
}
}
onImageLoad(): void {
this.imageNaturalWidth = this.svgImage.nativeElement.naturalWidth;
this.imageNaturalHeight = this.svgImage.nativeElement.naturalHeight;
this.fitImageToContainer();
}
private fitImageToContainer(): void {
const containerRect = this.container.nativeElement.getBoundingClientRect();
const containerAspectRatio = containerRect.width / containerRect.height;
const imageAspectRatio = this.imageNaturalWidth / this.imageNaturalHeight;
const isRotated = this.rotation % 180 !== 0;
const effectiveImageAspectRatio = isRotated ? 1 / imageAspectRatio : imageAspectRatio;
if (containerAspectRatio > effectiveImageAspectRatio) {
// 容器較寬,以高度為基準
this.zoom = containerRect.height / (isRotated ? this.imageNaturalWidth : this.imageNaturalHeight);
} else {
// 容器較高,以寬度為基準
this.zoom = containerRect.width / (isRotated ? this.imageNaturalHeight : this.imageNaturalWidth);
}
// 確保整個圖像都在視圖中
this.zoom = Math.min(1, this.zoom);
// 重置平移位置到中心
this.panX = 0;
this.panY = 0;
// 保存初始狀態
this.initialZoom = this.zoom;
this.initialPanX = this.panX;
this.initialPanY = this.panY;
this.updateTransform();
}
}