Во второй части мы займемся созданием возможности сохранения рисунка, добавим возможность редактирования наших сохраненных рисунков, а также добавим возможность просмотра пользователем примеров работ, по которым при желании можно будет создавать такие же или лучше. Первую часть смотрим здесь. Для начала создадим интерфейс наших данных, которые мы будем использовать при взаимодействии с базой данных.

ng g i shared/interfaces/canvasPostnterface.interface.ts и поместим в него:

export interface CanvasPostInterface {
  _id?: string,
  name: string;
  image: string;
  canvas: string | [];
}

Далее создадим сервис,  реализующий классический CRUD, но перед этим создадим два вспомогательных элемента, а именно Базовый сервис от которого мы будем наследовать наш сервис и абстрактный класс методы которого мы будем реализовывать в нашем сервисе. Создаем наш родительский сервис ng g s shared/services/root со следующим кодом:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment.dev';

@Injectable({
  providedIn: 'root'
})
export class RootService {

  protected apiUrl = с.apiUrl;

  constructor(
    protected http: HttpClient,
  ) { }

}

Не Забываем настроить environment, добавляем в environment/environment.dev.ts

apiUrl: 'localhost/api'

Создаем абстрактный класс в shared/abstract/abstract-base-service.abstarct.ts

import { Observable } from 'rxjs';

export abstract class AbstractBaseService {
 public abstract getData(): Observable<any>;
 public abstract getDataById(): Observable<any>;
 public abstract update(): Observable<any>;
 public abstract create(): Observable<any>;
 public abstract destroy(): Observable<Response>;
}

Теперь создаем сервис canvas-post в котором мы должны реализовать все абстрактные методы и в котором нам будут доступны все свойства базового класса ng g s shared/services/canvas-post, пишем в canvas-post следующий код:

import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AbstractBaseService } from '../abstract/abstract-base-service.abstarct';
import { CanvasPostInterface } from '../interfaces/canvas-post.interface';
import { RootService } from './root.service';

@Injectable({
  providedIn: 'root'
})
export class CanvasPostService extends RootService implements AbstractBaseService {
  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json',
    }),
  };
  protected componentUrl = `${this.apiUrl}/Canvas`;

  getData(): Observable<CanvasPostInterface[]> {
    return this.http.get<CanvasPostInterface[]>(`${this.componentUrl}/canvas`);
  }

  getDataById(id: string): Observable<CanvasPostInterface> {
    return this.http.get<CanvasPostInterface>(`${this.componentUrl}/${id}`);
  }

  update(data: CanvasPostInterface): Observable<CanvasPostInterface> {
    const body = { data };
    return this.http.patch<CanvasPostInterface>(`${this.componentUrl}/${data._id}`, body);
  }

  create(data: CanvasPostInterface): Observable<CanvasPostInterface> {
    return this.http.post<CanvasPostInterface>(`${this.componentUrl}/create`, data,
    this.httpOptions);
  }
  
  destroy(id: number): Observable<Response> {
    return this.http.delete<any>(`${this.componentUrl}/${id}`);
  }
}

В дальнейшем у нас получится вот такая структура приложения:

После наших предыдущих приготовлений мы наконец-то реализуем возможность сохранения в базу данных наших рисунков по нажатию на кнопку “Сохранить в галерею”, для этого нам необходим создать обмен данными между компонентами, редактируем в main-canvas.component.html следующую строчку:

<app-canvas 
   [uploadSuccess]="uploadSuccess"  
   (pagesEventSave)="getSaveCanvas($event)"
>
</app-canvas>

после того как мы добавили

(pagesEventSave)="getSaveCanvas($event)"

в main-canvas.component.ts добавляем метод getSaveCanvas() где мы вызываем метод create нашего сервиса CanvasPostService отвечаючего за сохранение в базу данных, в дальнейшем мы к нему вернемся, а пока он выглядит, так:

constructor(private canvasPostService: CanvasPostService) { }

 getSaveCanvas(data: {}) {

   this.canvasPostService.create(data as CanvasPostInterface)
     .subscribe(respons=>{


   })

 }

в canvas.component.html добавляем кнопку Сохранить в галерею, после чего он он должен выглядеть, вот так:

<div class="w3-card-4">
 <div class="w3-container w3-center">
   <label for="colorWell"></label>
   
   <input list="" id="colorWell" type="color" name="colorRect" 
          [(ngModel)]="colorRect">

   <button (click)="create()">Востановить рисунок</button>
   <button (click)="clearPixel()">Задать пикселю цвет фона</button>
   <button (click)="save()">Сохранить в галерею</button>

 </div>
</div>

<div class="w3-card-4 layer">
 <div class="w3-container w3-center">
   
   <canvas id="canvas" #canvas></canvas>
   
 </div>
</div>

в canvas.component.ts создаем метод save где с помощью метода pagesEventSave мы передаем данные в метод getSaveCanvas() компонента main-canvas в котором вызывается сервис сохранения данных в базу, после чего его код выглядит вот так:

@Output() pagesEventSave: EventEmitter<{}>=new EventEmitter<{}>();
//добавляем переменную name
name: string;
//в метод onResize() добавляем this.name = data.name;
  onResize(data: any) {
    this.name = data.name;
    this.innerWidth = data.innerWidth;
    this.innerHeight = data.innerHeight;
    this.widthRect = data.widthRect;
    this.heightRect = data.heightRect;
    this.numberOf = data.numberOf;
    this.borderRow = data.borderRow;
    this.numberRow = data.numberRow;
    this.canvas.width = this.innerWidth;
    this.canvas.height = this.innerHeight
    this.colorfillStyle = data.colorfillStyle;
    this.cleardraw()
    this.draw()
  }
//создаем
 save() {
   const data = this.canvas?.toDataURL();
   this.pagesEventSave.emit({image: data, name: this.name, 
                             canvas: this.matr});
 }

не забываем импортировать в app.module.ts модуль HttpClientModule

Сейчас если мы попробуем  отправить данные мы получим ошибку, для избежания этого необходимо сериализовать наш массив matr, для этого используем JSON.stringify() 

this.pagesEventSave.emit({ image: data, name: this.name, canvas: JSON.stringify(this.matr) });

После того как данные сериализованы, мы можем отправлять их на сервер.

Добавляем слайдер.

Для этого создаем компонент ng g c canvas/components/slider 

Создаем в каталоге assets каталог images, скачиваем туда картинки

Добавляем в slider.components.html

<div class="w3-content w3-display-container">

 <img class="mySlides" src="assets/images/1.webp" style="width:400px">
 <img class="mySlides" src="assets/images/2.webp" style="width:400px">
 <img class="mySlides" src="assets/images/3.webp" style="width:400px">
 <img class="mySlides" src="assets/images/4.webp" style="width:400px">
 
 <button class="w3-button w3-black w3-display-left" (click)="plusDivs(-1)">
   &#10094;
  </button>
 <button class="w3-button w3-black w3-display-right" (click)="plusDivs(1)">
   &#10095;
  </button>

</div>

 Добавляем в slider.components.ts

 

import { Component, AfterViewInit } from '@angular/core';
 
@Component({
 selector: 'app-slider',
 templateUrl: './slider.component.html',
 styleUrls: ['./slider.component.scss']
})
export class SliderComponent implements AfterViewInit {
 
 slideIndex = 1;
 constructor(private elementRef: ElementRef) { }
 
 ngAfterViewInit(): void {
   this.showDivs(this.slideIndex);
 }
 
 plusDivs(n: number) {
   this.showDivs(this.slideIndex += n);
 }
 
 showDivs(n: number) {
   console.log(n)
   let i;
   let x = document.getElementsByClassName("mySlides");
   if (n > x.length) {
     this.slideIndex = 1
   }
   if (n < 1) { this.slideIndex = x.length }
   for (i = 0; i < x.length; i++) {
     (x[i] as HTMLElement).style.display = "none";
   }
   (x[this.slideIndex - 1] as HTMLElement).style.display = "block";
 }
}
 
/*метод showDivs(n: number) можно реализовать несколькими способами
с помощью getElementsByClassName или с помощью @ElementRef 
Напишем новый метод showDivsRef(n: number) где мы будем использовать 
ElementRef
перед этим внедрим зависимость
 constructor(private elementRef: ElementRef) { } 
*/

  showDivsRef(n: number) {
    // использованием document.getElementsByClassName
    let i;
    let element = this.elementRef.nativeElement.getElementsByClassName("mySlides");
    console.log(n, element)
    if (n > element.length) {
      this.slideIndex = 1
    }

    if (n < 1) { this.slideIndex = element.length }
    if (element.length) {
      for (i = 0; i < element.length; i++) {
        element[i].style.display = "none";
      }
      element[this.slideIndex - 1].style.display = "block";
    }
  }


Добавляем в left-panel.component.html после <div class="w3-third">

<div class="w3-white w3-text-grey w3-card-4">
   <div class="w3-container">
     <p class="w3-large"><b><i class="fa fa-asterisk fa-fw w3-margin-right w3-text-teal"></i>Галерея примеров</b>
     </p>
     <app-slider></app-slider>
     <hr>
   </div>
 </div><br>

Смотрим на результат

Реализуем сохранение рисунков для последующего редактирования.

Для того, что бы у нас были данные без использования базы данных мы создадим Mock.service и модель фейковых данных canvas.modelMock.ts, полный код на git , а структура класса без данных выглядит, так:

export class InMemoryCanvasModel { 
 get data() {
   return this._data;
 }
 _data = [
   {
     _id: ”61f0f9ad70c1c74fafc641f0”,
     name: "love",
     image: "love",
     canvas: ['для экономии места данные на git'],
}];
}

Получать наши фейковые данные будет наш canvas-post-mock.service.ts, которым мы заменим в компоненте main-canvas наш сервис CanvasPostMockService

import { InMemoryCanvasModel } from '../models/mock/canvas.modelMock';
 
@Injectable({
 providedIn: 'root'
})
export class CanvasPostMockService {
 private readonly list;
 private dataService: InMemoryCanvasModel;
 
 constructor() {
   this.dataService = new InMemoryCanvasModel();
   this.list = this.dataService.data ;
 } 
 getComponent(): Observable<CanvasPostInterface[]> {
   return of(this.list);
 } 
 getData(): Observable<CanvasPostInterface[]> {
   return of();
 } 
 getDataById(id: string): Observable<CanvasPostInterface> {
   return of();
 } 
 update(data: CanvasPostInterface): Observable<CanvasPostInterface> {
   return of();
 } 
 create(data: CanvasPostInterface): Observable<CanvasPostInterface> {
   return of();
 } 
 destroy(id: number): Observable<Response> {
   return of();
 }
}

Немного переделаем наш слайдер, чтобы была возможность передавать массив данных наших редактируемых изображений slide.component.html, что бы это выглядело так:

<div class="w3-content w3-display-container">
 <div *ngFor="let image of images">
   <img class="mySlides" src="{{image}}" style="width:400px">
 </div>
 <button class="w3-button w3-black w3-display-left" 
         (click)="plusDivs(-1)">&#10094;
  </button>
 <button class="w3-button w3-black w3-display-right" 
         (click)="plusDivs(1)">&#10095;
  </button>
</div>

Отредактируем slide.component.ts

//если данные не пришли значит берем эти данные 
images: CanvasPostInterface[] = [
    { _id: '', canvas: [], name: "", image: "assets/images/1.webp" },
    { _id: '', canvas: [], name: "", image: "assets/images/2.webp" },
    { _id: '', canvas: [], name: "", image: "assets/images/3.webp" },
    { _id: '', canvas: [], name: "", image: "assets/images/4.webp" }
  ];
  
@Input() imageInput: any;
……./*Если пришли данные то перезаписываем наш массив*/
 ngOnInit(): void {
   if (this.imageInput) {
     this.images = this.imageInput.map((data: any) => data.image)
   }
 }

Добавим в слайдер, галерею наших рисунков, для которых будет возможность редактирования main-canvas.component.html

<!-- images slider-->
<div class="w3-container w3-card w3-white w3-margin-bottom">
    <h2 class="w3-text-grey w3-padding-16">
      <i class="fa fa-paint-brush fa-fw w3-margin-right w3-xxlarge 
        w3-text-teal"></i>
      Поле рисования
    </h2>
   <div class="w3-container">
     //используем pip async
     <app-slider [imageInput]="canvasImg | async"></app-slider>
      
     <hr>
   </div>
 </div>
<!-- End images slider-->

Редактируем main.canvas.component.ts в дальнейшем сюда мы будем принимать наши рисунки из базы, а пока будем брать из фейковой модели:

//main.canvas.component.ts
canvasImg:  Observable<CanvasPostInterface[]>|undefined;
 constructor(private canvasPostService: CanvasPostService) { }
 ngOnInit(): void {
   this.canvasImg = this.canvasPostService.getComponent();
}

центрируем рисунок в slider, для этого добавим в slider.component.css немного стилей:

.mySlides {
 display:none;
} 
img.center-block {
 margin-left: auto;
 margin-right: auto;
}

Вот, что у нас должно получится:

Реализуем новую возможность рисования.

Как то не очень удобно рисовать кликами, по этому давайте добавим возможность закрашивать пиксели проводя над ними курсором, для того, чтобы получалось красиво эта функция будет доступна только с зажатой левой клавишей мыши. Для этого мы будем отслеживать событие mousemove, а закрашивать только если event.which равно единице, то есть нажата левая клавиша. А что бы не обрамлять код в условие, мы будем использовать отрицательную логику, а именно если клавиша не нажата, то ничего не делаем. 

//если клавиша не зажата выходим
if (event.which !== 1) {
  return;
}

Вот, что у нас получится.

this.rendererRef = this.renderer.listen(this.canvasRef.nativeElement,
                                        'mousemove', (event) => { 
       //если клавиша не зажата выходим
     if (event.which !== 1) {
       return;
     }

     let cX = event.offsetX;
     let cY = event.offsetY;
     const offsetLeft = this.canvasRef.nativeElement.offsetLeft;
     const offsetTop = this.canvasRef.nativeElement.offsetTop;
     this.canvasX = cX - offsetLeft;
     this.canvasY = cY - offsetTop;

     this.matr.data.map(data => {
       if (cX >= data.x && cX < data.x + this.numberOf && cY >= data.y 
           && cY < data.y + this.numberOf) {
         this.ctx.fillStyle = this.colorRect;
         this.ctx.fillRect(data.x, data.y, this.numberOf, this.numberOf);
         data.color = this.colorRect;
       }
     })
     this.canvasRef.nativeElement.getBoundingClientRect()
     localStorage.setItem('matr', JSON.stringify(this.matr));
     const data = this.canvas?.toDataURL();
   });

Теперь у нас в двух местах появился дублирующий  код, поэтому создадим дополнительный метод eventOfset(event: MouseEvent) и вынесем этот код туда.

eventOfset(event: MouseEvent) {
   let cX = event.offsetX;
   let cY = event.offsetY;
   
   const offsetLeft = this.canvasRef.nativeElement.offsetLeft;
   const offsetTop = this.canvasRef.nativeElement.offsetTop;
   this.canvasX = cX - offsetLeft;
   this.canvasY = cY - offsetTop;

   this.matr.data.map(data => {
     if (cX >= data.x && cX < data.x + this.numberOf && cY >= 
     data.y && cY < data.y + this.numberOf) {
       this.ctx.fillStyle = this.colorRect;
       this.ctx.fillRect(data.x, data.y, this.numberOf, this.numberOf);
       data.color = this.colorRect;
     }
   })
   this.canvasRef.nativeElement.getBoundingClientRect();
   localStorage.setItem('matr', JSON.stringify(this.matr));
   const data = this.canvas?.toDataURL();
 }

Соответственно внесем изменения в ngAfterViewInit():

  ngAfterViewInit() {
   ....
    this.rendererRef = this.renderer.listen(this.canvasRef.nativeElement, 'click', (event: MouseEvent) => {
     //добавили новый метод
      this.eventOfset(event)
    });
    this.rendererRef = this.renderer.listen(this.canvasRef.nativeElement, 'mousemove', (event) => {
      if (event.which !== 1) {
        return;
      }
       //добавили новый метод
      this.eventOfset(event);
    });

  }

Поскольку у нас несколько слайдеров, внесем некоторые дополнения которые помогут реагировать на событие только если это необходимо, ориентироваться будем на наличие данных  создаем в slider.component.ts переменную thereIsData = false, если данные пришли, то меняем в ngOnInit() ее значение на true. Сразу же создадим метод transferToСanvas() который будет отвечать за передачу нашего рисунка main-canvas, а после на наш холст.

Для этого в main-canvas.component.html редактируем строку внедрения в app-slider  и добавлем (pagesEventSlider)="dataSlider($event)", после чего это выглядит, так:

<app-slider (pagesEventSlider)="dataSlider($event)" 
  [imageInput]="canvasImg | async">
</app-slider>

 main-canvas.component.ts создаем метод dataSlider($event: {}) {   //TODO }

в slider.component.ts добавляем

@Output() pagesEventSlider: EventEmitter<{}> = new EventEmitter<{}>();

и соответственно редактируем метод transferToCanvas:

transferToCanvas(canvas: any) {
   if (!this.thereIsData) {     
   return;
   }
   this.pagesEventSlider.emit(canvas);
 }

Теперь по клику наши данные приходят в родительский для слайдера и холста компонент main-canvas.

Далее в canvas.component.ts создаем метод createTtransferToDataCanvas.

/* Метод воссоздания рисуноков из базы */
  createTtransferToDataCanvas(data: any) {
    this.cleardraw()
    this.matr = JSON.parse(data);
    const matr = JSON.parse(data);
    this.canvas.width = matr.innerWidth;
    this.canvas.height = matr.innerHeight;
    this.innerWidth = matr.innerWidth;
    this.innerHeight = matr.innerHeight;
    JSON.parse(data).data.map((data: any) => {
      this.ctx.fillStyle = data.color;
      this.ctx.fillRect(data.x, data.y, this.matr.numberOf,
                        this.matr.numberOf);
    })
  }

 В этом же компоненте добавляем:

  @Input() transferToDataCanvas: EventEmitter<{}>;
/*Добавляем подписку на наш transferToDataCanvas: EventEmitter 
в ngOnInit()*/

ngOnInit(): void {
    this.uploadSuccess.subscribe(data => {
      this.onResize(data);
    });  
    this.transferToDataCanvas.subscribe(data => {
      this.createTtransferToDataCanvas(data);
    });
  }

Переходим в main-canvas.component.html и редактируем:

<app-canvas 
  [transferToDataCanvas]="transferToDataCanvas"  
  [uploadSuccess]="uploadSuccess" 
  (pagesEventSave)="getSaveCanvas($event)"
></app-canvas>

Переходим в main-canvas.component.ts создаем эмиттер:

import { Component, EventEmitter, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { CanvasPostInterface } from 'src/app/shared/interfaces/canvas-post.interface';
import { CanvasPostMockService as CanvasPostService } from 'src/app/shared/services/canvas-post-mock.service';
//import { CanvasPostService   } from 'src/app/shared/services/canvas-post.service';

@Component({
  selector: 'app-main-canvas',
  templateUrl: './main-canvas.component.html',
  styleUrls: ['./main-canvas.component.scss']
})
export class MainCanvasComponent implements OnInit {

  uploadSuccess: EventEmitter<any> = new EventEmitter();
  canvasImgSuccess: EventEmitter<any> = new EventEmitter();
  //создаем эмиттер transferToDataCanvas
  transferToDataCanvas: EventEmitter<any> = new EventEmitter();
  canvasImg: Observable<CanvasPostInterface[]> | undefined;
  
  constructor(private canvasPostService: CanvasPostService) { }

  ngOnInit(): void {
    this.canvasImg = this.canvasPostService.getComponent();
    this.canvasPostService.getComponent().subscribe(respons => {
      console.log(respons[0].image)
    });   
  }

  /* Передаем данные из формы в канвас*/
  getDataPanel($event: FormGroup) {
    const data = {
      widthRect: Number($event.value.widthCanvas),
      heightRect: Number($event.value.heightCanvas),
      numberOf: Number($event.value.hwPixel),
      borderRow: $event.value.meshThickness,
      numberRow: Number($event.value.hwPixel) + Number($event.value.meshThickness),
      innerWidth: Number($event.value.heightCanvas) * (Number($event.value.hwPixel) + Number($event.value.meshThickness)),
      innerHeight: Number($event.value.widthCanvas) * (Number($event.value.hwPixel) + Number($event.value.meshThickness)),
      colorfillStyle: $event.value.colorFone
    }
    this.uploadSuccess.emit(data);//передали
  }  

  /* Принимает данные из слайдера и перадет в канвас*/
  dataSlider(data: {}) {
    console.log(data)
    this.transferToDataCanvas.emit(data);//передали
  }

  /* Принимает данные из панели и перадет в канвас*/
  pagesEventInMain(data: {}) {
    this.transferToDataCanvas.emit(data);//передали
  }
}

Проверяем, наш предварительно сохраненный рисунок по клику на рисунок, на панели Мои работы появляется новый холст. Отлично.

Создаем TAB элемент.

Сейчас наше приложение выглядит немного не эстетично.

Для придания более опрятного вида, реализуем Tab элемент куда перенесем Форму создания холста, Галерею примеров и Мои работы. Вот как это будет выглядеть:

Редактируем left-panel.component.html, что бы код в нем выглядел вот так:

<div class="w3-third" style="max-width: 600px;">

  <!--TAB-->
  <div class="w3-bar w3-black">
    <button class="w3-bar-item w3-button" (click)="openTab('one')">
      Форма создания холста
    </button>
    <button class="w3-bar-item w3-button" (click)="openTab('two')">
      Мои работы
    </button>
    <button class="w3-bar-item w3-button" (click)="openTab('three')">
      Галерея примеров
    </button>
  </div>

  <div id="one" class="tab" >
    <div class="w3-white w3-text-grey w3-card-4">
      <div class="w3-container">
        <p class="w3-large"><b><i class="fa fa-asterisk fa-fw w3-margin-right w3-text-teal"></i>Форма создания
            холста</b>
        </p>
        <div class="w3-light-grey w3-round-xlarge w3-small">
          <app-form (pagesEvent)="getData($event)"></app-form>
        </div>
        <hr>
      </div>
    </div><br>
  </div>

  <div id="two" class="tab" style="display:none">
    <div class="w3-white w3-text-grey w3-card-4">
      <div class="w3-container">
        <p class="w3-large"><b><i class="fa fa-asterisk fa-fw w3-margin-right w3-text-teal"></i>Мои работы</b>
        </p>
        <app-slider 
          (pagesEventSlider)="dataSlider($event)" 
          [imageInput]="canvasImg">
        </app-slider>
        <hr>
        <hr>
      </div>
    </div><br>
  </div>

  <div id="three" class="tab" style="display:none">
    <div class="w3-white w3-text-grey w3-card-4">
      <div class="w3-container">
        <div class="w3-white w3-text-grey w3-card-4">
          <div class="w3-container">
            <p class="w3-large"><b><i class="fa fa-asterisk fa-fw w3-margin-right w3-text-teal"></i>Галерея примеров</b>
            </p>
            <app-slider></app-slider>
            <hr>
          </div>
        </div><br>
      </div>
    </div><br>
  </div>

  <!--  -->
</div>

Добавляем метод управления вкладками openTab() в left-panel.component.ts

import { Component, ElementRef, EventEmitter, Input, OnInit, Output } 
from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { CanvasPostInterface } 
from 'src/app/shared/interfaces/canvas-post.interface';

@Component({
  selector: 'app-left-panel',
  templateUrl: './left-panel.component.html',
  styleUrls: ['./left-panel.component.scss']
})
export class LeftPanelComponent implements OnInit {

  @Output() pagesEvent: EventEmitter<FormGroup> =
    new EventEmitter<FormGroup>();

  @Output() pagesEventLeftPanel: EventEmitter<{}> =
    new EventEmitter<{}>();

  @Input() imageInput: any;
  canvasImg: any;

  constructor(private elementRef: ElementRef) { }

  ngOnInit(): void {
    this.canvasImg = this.imageInput;//передали работы в слайдер
  }

  getData($event: FormGroup) {
    this.pagesEvent.emit($event);
  }

  dataSlider(data: {}) {
    this.pagesEventLeftPanel.emit(data);
   //передали в main
  }
/*Добавили метод управления вкладками*/
  openTab(tabId: string) {
    let i;
    let element = this.elementRef.nativeElement
      .getElementsByClassName("tab");
    for (i = 0; i < element.length; i++) {
      element[i].style.display = "none";
    }
    document.getElementById(tabId).style.display = "block";
  }

}

Вот в принципе и все. Хотя, давайте в завершение еще реализуем небольшое улучшение дающее нам возможность убирать сетку. Для этого добавим новый метод resizePixel() в canvas.component.ts после чего наш компонент примет следующий вид:

import { AfterViewInit, Component, ElementRef, 
        EventEmitter, Input, OnInit, Output, Renderer2, ViewChild } 
from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-canvas',
  templateUrl: './canvas.component.html',
  styleUrls: ['./canvas.component.scss']
})
export class CanvasComponent implements AfterViewInit, OnInit {

  @Output() pagesEventSave: EventEmitter<{}> =
    new EventEmitter<{}>();

  canvas: HTMLCanvasElement;
  innerWidth: number;
  innerHeight: number;
  rendererRef: any;
  numberRow: number;
  numberOf = 10; //размер пикселя
  borderRow = 1;
  widthRect = 50;
  heightRect = 50;
  x = 0;
  y = 0
  canvasX: number // X click cordinates
  canvasY: number // Y click cordinates
  colorRect = '#242323';//цвет пикселя рисовалки
  colorfillStyle = '#19a923';//цвет пикселя холста
  matr = { innerWidth: 0, innerHeight: 0, numberOf: 10, backgroundColor: '#19a923', data: [{ x: 0, y: 0, color: '' }] }
  name: string;
  private ctx: CanvasRenderingContext2D | null;
  @ViewChild('canvas') canvasRef: ElementRef;
  @Input() uploadSuccess: EventEmitter<FormGroup>;
  @Input() transferToDataCanvas: EventEmitter<{}>;

  constructor(private el: ElementRef,
    private renderer: Renderer2,
  ) {
    this.numberRow = this.numberOf + this.borderRow;
    this.innerWidth = this.heightRect * this.numberRow;
    this.innerHeight = this.widthRect * this.numberRow;
  }

  onResize(data: any) {
    this.name = data.name;
    this.innerWidth = data.innerWidth;
    this.innerHeight = data.innerHeight;
    this.widthRect = data.widthRect;
    this.heightRect = data.heightRect;
    this.numberOf = data.numberOf;
    this.borderRow = data.borderRow;
    this.numberRow = data.numberRow;
    this.canvas.width = this.innerWidth;
    this.canvas.height = this.innerHeight
    this.colorfillStyle = data.colorfillStyle;
    this.cleardraw()
    this.draw()
  }
  

  ngOnInit(): void {
    this.uploadSuccess.subscribe(data => {
      this.onResize(data);
    });
    this.transferToDataCanvas.subscribe(data => {
      this.createTtransferToDataCanvas(data);
    });
  }

  ngAfterViewInit() {
    this.canvas = this.canvasRef.nativeElement;
    this.canvas.width = this.innerWidth;
    this.canvas.height = this.innerHeight;
    this.cleardraw()
    this.draw();
    const data = this.canvas?.toDataURL();

    if (this.rendererRef != null) {
      this.rendererRef();
    }

    this.rendererRef = this.renderer
      .listen(this.canvasRef.nativeElement, 
              'click', (event: MouseEvent) => {
      this.eventOfset(event)
    });

    this.rendererRef = this.renderer
      .listen(this.canvasRef.nativeElement, 
              'mousemove', (event) => {
      if (event.which !== 1) {
        return;
      }
      this.eventOfset(event);
    });

  }

  draw(): void {
    this.matr.innerWidth = this.innerWidth;
    this.matr.innerHeight = this.innerHeight;
    this.matr.backgroundColor = this.colorfillStyle;
    this.matr.numberOf = this.numberOf;
    for (let i = 0; i < this.heightRect; i++) {
      for (let j = 0; j < this.widthRect; j++) {
        this.ctx.fillStyle = this.colorfillStyle;
        this.ctx.fillRect(j * this.numberRow, i * this.numberRow, 
                          this.numberOf, this.numberOf);
        this.matr.data.push({ x: j * this.numberRow, 
                             y: i * this.numberRow, 
                             color: this.colorfillStyle })
      }
    }

  }

  cleardraw(): void {
    this.ctx = this.canvas.getContext('2d');
    this.ctx.clearRect(0, 0, this.widthRect, this.heightRect);
    this.matr.data = [];
  }

  createFromLocalStorage(): void {
    const retrievedObject = localStorage.getItem('matr');
    this.matr = JSON.parse(retrievedObject);
    this.matr.data.map(data => {
      this.ctx.fillStyle = data.color;
      this.ctx.fillRect(data.x, data.y, this.matr.numberOf, 
                        this.matr.numberOf);
    })
  }

  /* Метод воссоздания рисуноков из базы */
  createTtransferToDataCanvas(data: any): void {
    this.cleardraw()
    this.matr = JSON.parse(data);
    const matr = JSON.parse(data);
    this.canvas.width = matr.innerWidth;
    this.canvas.height = matr.innerHeight;
    this.innerWidth = matr.innerWidth;
    this.innerHeight = matr.innerHeight;
    JSON.parse(data).data.map((data: any) => {
      this.ctx.fillStyle = data.color;
      this.ctx.fillRect(data.x, data.y, this.matr.numberOf, 
                        this.matr.numberOf);
    })
  }
  
  /* изменить размер пикселя */
  resizePixel(): void {
    this.matr.data.map(data => {
      this.ctx.fillStyle = data.color;
      this.ctx.fillRect(data.x, data.y, this.matr.numberOf + 1, 
                        this.matr.numberOf + 1);
    })
  }

  /* задаем цвет фону */
  clearPixel(): void {
    this.colorRect = this.matr.backgroundColor
  }
  /* сохраняем */
  save(): void {
    const data = this.canvas?.toDataURL();
    this.pagesEventSave.emit({ image: data, name: this.name, 
                              canvas: JSON.stringify(this.matr) });
  }

  /* функция рисования */
  eventOfset(event: MouseEvent): void {
    let cX = event.offsetX;
    let cY = event.offsetY;
    const offsetLeft = this.canvasRef.nativeElement.offsetLeft;
    const offsetTop = this.canvasRef.nativeElement.offsetTop;
    this.canvasX = cX - offsetLeft;
    this.canvasY = cY - offsetTop;

    this.matr.data.map(data => {
      if (cX >= data.x && cX < data.x + this.numberOf && cY >= data.y 
          && cY < data.y + this.numberOf) {
        this.ctx.fillStyle = this.colorRect;
        this.ctx.fillRect(data.x, data.y, this.numberOf, this.numberOf);
        data.color = this.colorRect;
      }
    })
    this.canvasRef.nativeElement.getBoundingClientRect()
    localStorage.setItem('matr', JSON.stringify(this.matr));
    const data = this.canvas?.toDataURL();
  }
}

Создадим кнопку по нажатию на которую будет убиратся сетка в canvas.component.html после чего его код будет выглядеть так:

<div class="w3-card-4">
  <div class="w3-container w3-center">
    <label for="colorWell"></label>
    <input list="" id="colorWell" type="color" name="colorRect" [(ngModel)]="colorRect">
    <button (click)="createFromLocalStorage()">Востановить рисунок</button>
    <button (click)="clearPixel()">Задать пикселю цвет фона</button>
    <button (click)="save()">Сохранить в галерею</button>
    <button (click)="resizePixel()">Убрать сетку</button>
  </div>
</div>
<div class="w3-card-4 layer">
  <div class="w3-container w3-center"  style="padding: 30px;">
    <canvas id="canvas" #canvas></canvas>
  </div>
</div>

Проверяем, нажимаем на кнопку Убрать сетку

сетка пропала, и наш слоник выглядит абсолютно по другому,

все работает.

Поскольку часто приходится работать сразу с несколькими проектами, давайте внесем изменения для запуска нашего приложения на порте 4300, для этого в angular.json внесем такие вот изменения:

    "defaultConfiguration": "production"
        },
        "serve": {
          "options": {
            "port": 4300
          },
          "builder": "@angular-devkit/build-angular:dev-server",

Примечание, проект запускается на 4300 порту. На сегодня все. Продолжение следует.

Демо, git

Комментарии (5)


  1. Xuxicheta
    30.01.2022 21:26
    +1

    А теперь в качестве прикола попробуйте strict режим в tsconfig включить.


    1. resintegra Автор
      30.01.2022 21:54
      -1

      Флаг strict напрямую связан с проверкой типов. Его включение автоматически активирует абсолютно все флаги секции Strict Checks, включая и alwaysStrict. У такого подхода есть как минимум один недостаток – неочевидность. Устанавливая strict: true, нет наглядного представления, какие именно проверки включены и какие опции вообще существуют. Для проектов, которые с самого начала пишутся на TypeScript это не так принципиально, как для проектов, которые поэтапно портируются с JavaScript.

      В процессе портирования существующего приложения нет возможности сразу включить все проверки. Приходится активировать их по одной шаг за шагом. Иногда даже случаются сложности, из-за которых приходится откатывать ранее установленные флаги обратно в false.

      Есть небольшая особенность работы флага strict – список подконтрольных ему флагов может пополняться по мере выхода новых версий TypeScript. Подобные моменты если случаются, то редко и всегда освещаются в release notes если, конечно, вы их читаете перед обновлением версии.

      Лично я предпочитаю указывать список флагов явным образом


      1. Xuxicheta
        30.01.2022 23:22

        Это хорошо что это все вы знаете, но это был намек, как улучшить код.

        Просто не хочется сразу кидаться с критикой. Пока ничего такого, ради чего стоило писать статью, не видно.


        1. resintegra Автор
          31.01.2022 10:15

          На самом деле я вам очень благодарен, хотя бы за то, что вы не только обратили внимание на мое творчество, а еще и написали объективные комментарии. Планировалось провести через весь цикл, показать черновую работу и я думаю кому то это будет полезно, ведь добавляет же кто то посты в закладки. В свое время я искал примеры создания проектов на Angular и их было очень мало. Сегодня я пришел к тому, что мне хочется показать пример создания приложения которое кардинально отличается от тудушек и кликеров, даже если это и не перевод, а личное творчество. Для себя я сделал вывод, что писать на такие темы очень сложно, но это не уменьшает их ценности. К тому же, я внимательно ознакомился с правилами и очень рад, что вы следуете этому правилу: “Не злоупотребляйте своей возможностью голосования. Необходимо понимать, что минус сильно отличается от плюса — минус угнетает человека, а не развивает его. Ставьте плюсы, когда вам что-то нравится, но подумайте, прежде чем ставить минус, если что-то не понравилось”.

          Ну и мои любимые правила Хабра гласят - Минус — это не аргумент, и, тем более, не контраргумент. Не делай другим то, что не хочешь получить от них сам. Помогайте другим там, где вы это можете делать.


  1. Bekentiy
    30.01.2022 21:35

    Пишем Pixel Art Maker на JavaScript