В этой статье я собираюсь показать создание небольшого приложения с помощью которого можно будет создавать пиксельные картинки, также будет возможность разместить эти картинки в галерее, проголосовать за понравившуюся. Вообщем будет блог, галерея и мастерская. Мне показалось, что это может быть интересно новичкам, которые уже немного научились Angular и пробуют делать различные приложения для того, чтобы выработать свой стиль и набить руку.  

Это моя первая статья подобного рода и возможно я бы никогда ее не разместил, но моё лузерство и привычка быть обыкновенным, меня настолько раскрепостили, что я воспринимаю поражения как должное. Амбиций нет, есть только кайф от того, что я делаю, люблю созерцать, люблю что то создавать. Рад буду если это кому то принесет пользу. Надеюсь при просмотре опытными разработчиками все не окажется очень плохо и я найду в себе силы выложить следующие части.

А может кому то просто понравится рисовать, ссылка на демо ниже.

В этой части я буду использовать Angular и CSS фреймворк от  w3schools

Итак, в первой части будет описан процесс создания вот такой мастерской.

Здесь можно посмотреть Demo, здесь находится код.

Идем устанавливаем Ангулар. Создаем новый проект.

Подразумевается, что вы умеете это делать по этому я не буду это описывать.

Наша мастерская будет создана в виде отдельного модуля.

Создаем его с помощью следующих команд:

ng g m canvas

ng g c canvas/components/main-canvas --module canvas.module

ng g c canvas/components/left-panel --module canvas.module

ng g c canvas/components/canvas --module canvas.module

ng g c canvas/components/form --module canvas.module

Где main-canvas у нас является родителем для компонентов left-panel, canvas, form.

Создаем component main-canvas

code main-canvas.component.html

<!DOCTYPE html>
<html>
<title>W3.CSS Template</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto'>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>
 html,
 body,
 h1,
 h2,
 h3,
 h4,
 h5,
 h6 {
   font-family: "Roboto", sans-serif
 }
</style>
<div class="w3-bar w3-light-grey">
 <h1 style="padding-left: 2rem;">ARTpixel</h1>
</div> 
<body class="w3-light-grey">
 <!-- Page Container -->
 <div class="w3-content w3-margin-top" style="max-width:1400px;">
   <!-- The Grid -->
   <div class="w3-row-padding">
     <!-- Right Column -->     
     
     <!-- Left Column app-left-panel-->
     <app-left-panel (pagesEvent)="getDataPanel($event)"></app-left-panel>
     <!-- End Left Column app-left-panel-->
          
     <div class="w3-twothird">
       <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">
           
           <!-- Canvas app-canvas-->
           <app-canvas [uploadSuccess]="uploadSuccess"></app-canvas>
           <!-- End Canvas app-canvas-->
           
           <hr>
         </div>
       </div> 
     </div>
     <!-- End Right Column -->
   </div>
   <!-- End Grid -->
 </div> 
 <!-- End Page Container --> 
 <footer class="w3-container w3-teal w3-center w3-margin-top">
   <p>Find me on social media.</p>
   <i class="fa fa-facebook-official w3-hover-opacity"></i>
   <i class="fa fa-instagram w3-hover-opacity"></i>
   <i class="fa fa-snapchat w3-hover-opacity"></i>
   <i class="fa fa-pinterest-p w3-hover-opacity"></i>
   <i class="fa fa-twitter w3-hover-opacity"></i>
   <i class="fa fa-linkedin w3-hover-opacity"></i>
   <p>Powered by <a href="https://www.w3schools.com/w3css/default.asp" target="_blank">w3.css</a></p>
 </footer>
</body>
 
</html>

code main-canvas.component.ts

import { Component, EventEmitter } from '@angular/core';
import { FormGroup } from '@angular/forms';
 
@Component({
 selector: 'app-main-canvas',
 templateUrl: './main-canvas.component.html',
 styleUrls: ['./main-canvas.component.scss']
})
export class MainCanvasComponent {
 uploadSuccess: EventEmitter<any> = new EventEmitter();//объявили
 
 constructor() { }
 
 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);//передали
 }
}

Пока в этом компоненте у нас находится единственный метод, который получает данные из компонента left-panel  

<app-left-panel (pagesEvent)="getDataPanel($event)"></app-left-panel>

который в свою очередь получает их из компонента form  и передает их в компонент canvas

<app-canvas [uploadSuccess]="uploadSuccess"></app-canvas> 

где на основании отправленных данных  происходит создание рисунка. 

Создаем компонент  left-panel 

left-panel.cpmponent.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>
           <div class="w3-light-grey w3-round-xlarge w3-small">
               <app-form
               (pagesEvent)="getData($event)"
               ></app-form>
           </div>
           <hr>
       </div>
   </div><br>
</div>

Здесь все просто  мы получаем данные из компонента форм

<app-form(pagesEvent)="getData($event)"></app-form>

left-panel.component.ts

import { Component, EventEmitter, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
 
@Component({
 selector: 'app-left-panel',
 templateUrl: './left-panel.component.html',
 styleUrls: ['./left-panel.component.scss']
})
export class LeftPanelComponent {
 
 @Output() pagesEvent: EventEmitter<FormGroup > =
   new EventEmitter< FormGroup >();
 constructor() { }
 getData($event: FormGroup) {
   this.pagesEvent.emit($event);
 }
}

Сейчас у нас здесь единственный метод getData() который передает полученный из формы данные в родительский компонент и дальше в канвас компонент

 Создаем компонент form.  

form.component.html

<form [formGroup]="form" class="w3-container">
 <p>
   Холст
 </p>
 <label>Количество пикселей по горизонтали</label>
 <input class="w3-input" type="number" name="widthCanvas" formControlName="widthCanvas">
 
 <label>Количество пикселей по вертикали</label>
 <input class="w3-input" type="number" name="heightCanvas" formControlName="heightCanvas">
 <p>
   Pixel
 </p>
 <label>Размер пикселя</label>
 <input class="w3-input" type="number" name="hwPixel" formControlName="hwPixel">
 <label>Толщина сетки</label>
 <input class="w3-input" type="number" name="meshThickness" formControlName="meshThickness">
 <label>Цвет заливки холста</label>
 <input class="w3-input w3-border 0 input-color" type="color" name="colorFone" formControlName="colorFone">
 <hr>
 
 <div class="form-group">
   <button class="w3-button w3-pale-red" (click)="onCreate()">
     Создать
   </button>
 </div>
 <div>
   <hr>
 </div>
 
</form>

Создаем form.component.ts

import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
 
@Component({
 selector: 'app-form',
 templateUrl: './form.component.html',
 styleUrls: ['./form.component.scss']
})
export class FormComponent implements OnInit {
 
 @Output() pagesEvent: EventEmitter<FormGroup> = new EventEmitter<FormGroup>();
 constructor() { }
 
 form: FormGroup = new FormGroup(
   {
     "heightCanvas": new FormControl("24", Validators.required),
     "widthCanvas": new FormControl("24", Validators.pattern("[0-9]{10}")),
     "hwPixel": new FormControl("10", Validators.required),
     "meshThickness": new FormControl("1", Validators.pattern("[0-9]{10}")),
     "colorFone": new FormControl("green"),
   }
 ); 
 
 onCreate(): void {
   this.pagesEvent.emit(this.form);
 }
}
 

и наконец создаем компонент нашего ядра canvas.component

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>
 
 </div>
</div>
<div class="w3-card-4 layer">
 <div class="w3-container w3-center">
   <canvas id="canvas" #canvas></canvas>
 </div>
</div>

canvas.component.ts

import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, 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 {
 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 = { numberOf: 10, backgroundColor: '#19a923', data: [{ x: 0, y: 0, color: '' }] }
 
 private ctx: CanvasRenderingContext2D | null;
 @ViewChild('canvas') canvasRef: ElementRef;
 
 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.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()
 }
@Input() uploadSuccess: EventEmitter<FormGroup>;
 
 ngOnInit(): void {
   this.uploadSuccess.subscribe(data => {
     this.onResize(data);
   });
 }

ngAfterViewInit(): void {
   this.canvas = this.canvasRef.nativeElement;
   this.canvas.width = this.innerWidth;
   this.canvas.height = this.innerHeight;
   let image = document.getElementById('source');
   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) => {
     let cX = event.layerX;
     let cY = event.layerY;
     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;
       }
     })
     localStorage.setItem('matr', JSON.stringify(this.matr));
     const data = this.canvas?.toDataURL();
   })
 }
 
 draw(): void {
   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 = [];
 }
 
 create(): 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);
   })
 }
 
 clearPixel(): void {
   this.colorRect = this.matr.backgroundColor
 }
}

А теперь давайте разбираться что здесь происходит. Компонент состоит из методов:

onResize(data: any), draw(), cleardraw(), create(), clearPixel()

onResize(data: any) - вызывается из метода ngOnInit, когда приходит событие о том, что пользователь отправил форму создания холста.

cleardraw(): подготавливает поле к перерисовке

draw(): отрисовывает холст

create(): восстанавливает рисунок из localStorage

clearPixel(): устанавливает цвет кисти равным цвету холста

ngOnInit: вызывается фреймворком Angular один раз после установки свойств компонента, которые участвуют в привязке. Выполняет инициализацию компонента. И здесь мы подписываемся на события из родительского компонента

ngAfterViewInit: вызывается фреймворком Angular после инициализации представления компонента, а также представлений дочерних компонентов. Вызывается только один раз, здесь мы подписываемся на пользовательское событие клика и собственно здесь и происходит рисование

На сегодня это все, иду готовить продолжение.

P/S/ баг нашли, вот чинили Не взирая на мышинную возню event.layer in Firefox and Chrome в матрице все спокойно

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