iT邦幫忙

2024 iThome 鐵人賽

DAY 7
1
佛心分享-SideProject30

收納規劃APP系列 第 7

Day7:加入平面圖

  • 分享至 

  • xImage
  •  

功能拆開的時候都很快樂,合併的時候才知道合不合適
就算有先規劃順序,但還是有種瞎子摸象的感覺

操作環境跟程式碼 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();
  }
}

上一篇
Day6:ngx-moveable 變形功能之三角形處理 (同場加映Canvas)
下一篇
Day8:平面圖疊家具(一)
系列文
收納規劃APP32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言