src/app/canvas.component.ts (95 lines of code) (raw):

/** * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Component, ElementRef, HostListener, input, ViewChild, afterNextRender, viewChild } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-canvas', standalone: true, template: ` <mat-icon (click)="erase()" aria-hidden="false" aria-label="Erase" fontIcon="delete_outline"></mat-icon> <canvas [width]="width()" [height]="height()" #canvas></canvas> `, imports: [MatIconModule], styles: ` canvas { border: 1px solid gray; } :host { position: relative; display: inline-block; } mat-icon { position: absolute; top: 0px; left: 0px; cursor: pointer; opacity: 0; transition: opacity 0.2s; } mat-icon:hover { opacity: 1; } ` }) export class CanvasComponent { width = input(500); height = input(500); private canvas = viewChild<ElementRef<HTMLCanvasElement>>('canvas'); private drawing = false; private pointBuffer: { x: number; y: number }[] = []; constructor() { afterNextRender(() => { this.erase(); }); } @HostListener('mousedown', ['$event']) private startDrawing() { this.drawing = true; } @HostListener('mousemove', ['$event']) private draw(event: MouseEvent) { if (!this.drawing) return; if (!this.canvas()) return; const el = this.canvas()!.nativeElement as HTMLCanvasElement; const context = el.getContext('2d'); if (!context) return; const rect = el.getBoundingClientRect(); this.pointBuffer.push({ x: event.x - rect.left, y: event.y - rect.top, }); if (this.pointBuffer.length < 3) { return; } context.moveTo(this.pointBuffer[0].x, this.pointBuffer[0].y); context.bezierCurveTo( this.pointBuffer[0].x, this.pointBuffer[0].y, this.pointBuffer[1].x, this.pointBuffer[1].y, this.pointBuffer[2].x, this.pointBuffer[2].y ); this.pointBuffer.shift(); context.stroke(); } @HostListener('document:mouseup') private stopDrawing() { this.drawing = false; this.pointBuffer = []; } getBase64Drawing() { if (!this.canvas()) return null; return (this.canvas()!.nativeElement as HTMLCanvasElement).toDataURL().replace('data:image/png;base64,', ''); } erase() { if (!this.canvas()) return; const el = this.canvas()!.nativeElement as HTMLCanvasElement; const context = el.getContext('2d'); if (!context) return; this.pointBuffer = []; context.reset(); context.strokeStyle = '#000000'; context.fillStyle = '#ffffff'; context.fillRect(0, 0, this.width(), this.height()); context.fill(); } }