🐷

【Angular】Trimming Icon by Image Cropper with Mateirl Design

2022/04/04に公開

Introduction

In this article, create the trimming icon dialog like here.

Install packages

Install @angular/material

$ ng add @angular/material

Install ngx-image-cropper

$ npm i ngx-image-cropper

Add index.html to effect material icon

<link
  href="https://fonts.googleapis.com/icon?family=Material+Icons"
  rel="stylesheet"
/>

Add style.scss to effect material theme

@import '~@angular/material/prebuilt-themes/indigo-pink.css';

Coding

image-cropper-dialog

image-cropper-dialog.ts

import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import {
  base64ToFile,
  ImageCroppedEvent,
  ImageTransform,
} from 'ngx-image-cropper';

@Component({
  selector: 'image-cropper-dialog',
  templateUrl: './image-cropper-dialog.html',
  styleUrls: ['./image-cropper-dialog.scss'],
})
export class ImageCropperDialog {
  imageChangedEvent: any = '';
  croppedImage: any = '';
  rotation: number = 0;
  scale: number = 1;
  showCropper: boolean = false;
  containWithinAspectRatio: boolean = true;
  transform: ImageTransform = {};
  defaultWidth: number;
  defaultHeight: number;

  constructor(
    @Inject(MAT_DIALOG_DATA) public data: any,
    private readonly dialogRef: MatDialogRef<ImageCropperDialog>,
    private readonly cd: ChangeDetectorRef
  ) {
    this.fileChangeEvent(data.event);
  }

  fileChangeEvent(event: any): void {
    this.imageChangedEvent = event;
  }

  applyIcon(): void {
    console.log('Cropped Image!', this.croppedImage);
  }

  resetImage() {
    this.scale = 1;
    this.rotation = 0;
    this.transform = {};
    this._resetCropperPosition();
  }

  imageCropped(event: ImageCroppedEvent) {
    this.croppedImage = base64ToFile(event.base64);
    // or if you want to save as base64
    // this.croppedImage = base64ToFile(event.base64);
  }

  imageLoaded() {
    this.showCropper = true;
  }

  rotateLeft() {
    this.rotation--;
    this._flipAfterRotate();
  }

  rotateRight() {
    this.rotation++;
    this._flipAfterRotate();
  }

  flipHorizontal() {
    this.transform = {
      ...this.transform,
      flipH: !this.transform.flipH,
    };
  }

  flipVertical() {
    this.transform = {
      ...this.transform,
      flipV: !this.transform.flipV,
    };
  }

  changeScale(e) {
    this.scale = e.value;
    this.transform = {
      ...this.transform,
      scale: this.scale,
    };
  }

  zoomOut() {
    this.scale -= 0.1;
    this.transform = {
      ...this.transform,
      scale: this.scale,
    };
  }

  zoomIn() {
    this.scale += 0.1;
    this.transform = {
      ...this.transform,
      scale: this.scale,
    };
  }

  toggleContainWithinAspectRatio() {
    this.containWithinAspectRatio = !this.containWithinAspectRatio;
  }

  closeEditIconDialog(): void {
    this.dialogRef.close();
  }

  private _flipAfterRotate() {
    const flippedH = this.transform.flipH;
    const flippedV = this.transform.flipV;
    this.transform = {
      ...this.transform,
      flipH: flippedV,
      flipV: flippedH,
    };
  }

  private _resetCropperPosition(): void {
    this.containWithinAspectRatio = false;
    this.cd.detectChanges();
    this.containWithinAspectRatio = true;
  }
}

image-cropper-dialog.html

<ng-container>
  <div class="mat-dialog-header">
    <div class="flex-start">
      <button mat-icon-button (click)="closeEditIconDialog()">
        <mat-icon>arrow_back</mat-icon>
      </button>
      <h1 mat-dialog-title class="mb-0">Tim Icon</h1>
    </div>

    <div class="my-8 action-header flex-center">
      <button mat-icon-button (click)="rotateLeft()">
        <mat-icon>rotate_left</mat-icon>
      </button>
      <button mat-icon-button (click)="rotateRight()">
        <mat-icon>rotate_right</mat-icon>
      </button>
      <button mat-icon-button (click)="flipHorizontal()">
        <mat-icon>swap_horiz</mat-icon>
      </button>
      <button mat-icon-button (click)="flipVertical()">
        <mat-icon>sync</mat-icon>
      </button>
    </div>
  </div>

  <mat-dialog-content>
    <image-cropper
      [imageChangedEvent]="imageChangedEvent"
      [maintainAspectRatio]="true"
      [containWithinAspectRatio]="containWithinAspectRatio"
      [aspectRatio]="1"
      [resizeToWidth]="256"
      [cropperMinWidth]="128"
      [onlyScaleDown]="true"
      [roundCropper]="false"
      [canvasRotation]="rotation"
      [transform]="transform"
      [alignImage]="'center'"
      [style.display]="showCropper ? null : 'none'"
      format="png"
      (imageCropped)="imageCropped($event)"
      (imageLoaded)="imageLoaded()"
    ></image-cropper>
  </mat-dialog-content>

  <div class="flex-center">
    <button mat-icon-button (click)="zoomOut()">
      <mat-icon>zoom_out</mat-icon>
    </button>
    <mat-slider
      step="0.1"
      min="1"
      max="2"
      [value]="scale"
      (change)="changeScale($event)"
    ></mat-slider>
    <button mat-icon-button (click)="zoomIn()">
      <mat-icon>zoom_in</mat-icon>
    </button>
  </div>

  <mat-dialog-actions class="flex-end">
    <button mat-button (click)="resetImage()">Reset</button>
    <button mat-button color="accent" (click)="applyIcon()">Apply</button>
  </mat-dialog-actions>
</ng-container>

image-cropper-dialog.scss

:host {
  .flex-start {
    display: flex;
    justify-content: start;
    text-align: center;
  }

  .flex-center {
    display: flex;
    justify-content: center;
    text-align: center;
  }

  .flex-end {
    display: flex;
    justify-content: end;
    text-align: center;
  }

  .action-header {
    background-color: #f5f5f5;
  }

  .mat-dialog-content {
    .cropper-container {
      width: 100%;
      margin: 0 auto;
    }
  }

  img {
    width: 600px;
  }
}

::ng-deep .ngx-ic-move {
  border-radius: 50%;
  outline: 1px solid #888;
}

Using cropper component (example: app.component)

app.component.ts

import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ImageCropperDialog } from './image-cropper-dialog/image-cropper-dialog';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(private readonly dialog: MatDialog) {}

  fileChangeEvent(event: any): void {
    this.dialog.open(ImageCropperDialog, {
      width: '600px',
      data: event,
    });
  }
}

app.component.html

<input type="file" (change)="fileChangeEvent($event)" />

app.module.ts

Import modules

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
+ import { ImageCropperModule } from 'ngx-image-cropper';
+ import { ImageCropperDialog } from './image-cropper-dialog/image-cropper-dialog';
+ import { MatButtonModule } from '@angular/material/button';
+ import { MatIconModule } from '@angular/material/icon';
+ import { MatDialogModule } from '@angular/material/dialog';
+ import { MatSliderModule } from '@angular/material/slider';
+ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,

+   MatButtonModule,
+   MatIconModule,
+   MatDialogModule,
+   MatSliderModule,
+   ImageCropperModule,

+   BrowserAnimationsModule,
  ],
  declarations: [
    AppComponent,
+   ImageCropperDialog
  ],
  bootstrap: [AppComponent],
+ entryComponents: [ImageCropperDialog],
})
export class AppModule {}

https://stackblitz.com/edit/image-cropper-f7hih7?file=app/app.component.ts

Discussion