Skip to content

Commit

Permalink
Implement zoom to fit, in, out.
Browse files Browse the repository at this point in the history
  • Loading branch information
max-vogler committed Mar 9, 2022
1 parent c1523a5 commit 3430bf4
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 35 deletions.
38 changes: 26 additions & 12 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
<div class="divider" *ngIf="canvas.hasImage"></div>
</div>

<div class="button-group zoom" *ngIf="canvas.hasImage">
<button mat-button (click)="canvas.zoomIn()">
<mat-icon>zoom_in</mat-icon>
</button>
<button mat-button (click)="canvas.zoomToFit()">
<mat-icon>fit_screen</mat-icon>
</button>
<button mat-button (click)="canvas.zoomOut()">
<mat-icon>zoom_out</mat-icon>
</button>
</div>

<mat-button-toggle-group
[hidden]="mode !== Mode.DRAW || !canvas.hasImage"
name="tool"
Expand Down Expand Up @@ -91,17 +103,19 @@
</div>
</mat-toolbar>

<app-canvas-editor
#canvas
[hidden]="mode !== Mode.DRAW || !(canvas.colorCounts | async)"
></app-canvas-editor>
<div class="scroll-container">
<app-canvas-editor
#canvas
[hidden]="mode !== Mode.DRAW || !(canvas.colorCounts | async)"
></app-canvas-editor>

<h2 *ngIf="!(canvas.colorCounts | async)">
Open, paste, or drop a small pixel art image.
</h2>
<h2 *ngIf="!(canvas.colorCounts | async)">
Open, paste, or drop a small pixel art image.
</h2>

<app-instructions
[hidden]="mode !== Mode.ASSEMBLE"
*ngIf="canvas.pixels | async as pixels"
[pixels]="pixels"
></app-instructions>
<app-instructions
[hidden]="mode !== Mode.ASSEMBLE"
*ngIf="canvas.pixels | async as pixels"
[pixels]="pixels"
></app-instructions>
</div>
25 changes: 19 additions & 6 deletions src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,25 @@
:host {
display: flex;
flex-direction: column;
align-items: center;
padding: 100px;
width: 100%;
height: 100%;
overscroll-behavior: contain;
}

.scroll-container {
flex: 1;
display: flex;
overflow: auto;

// Centering with justify-content breaks horizontal scrolling when zoomed in.
// Instead, children need to handle margins and centering.
& > * {
margin: 50px auto;
}
}

.tools {
background-color: white;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
column-gap: 10px;
Expand All @@ -44,6 +53,10 @@
background: #e0e0e0;
}

.button-group {
display: flex;
}

.spacer {
display: flex;
flex: 1;
Expand Down
2 changes: 2 additions & 0 deletions src/app/canvas-editor/canvas-editor.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@
canvas {
image-rendering: pixelated;
cursor: crosshair;

transition: width 200ms ease, height 200ms ease;
}
66 changes: 49 additions & 17 deletions src/app/canvas-editor/canvas-editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from '@angular/core';
import { EditableContext2D, HexColor } from './context';

const SCALE = 25;
const MAX_SCALE = 25;

export enum Tool {
DRAW,
Expand All @@ -43,7 +43,7 @@ export enum Tool {
export class CanvasEditorComponent implements OnInit, AfterViewInit {
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;

private ctx: EditableContext2D | null = null;
private ctx!: EditableContext2D;

@Input() activeTool: Tool = Tool.DRAW;

Expand All @@ -61,35 +61,67 @@ export class CanvasEditorComponent implements OnInit, AfterViewInit {

ngAfterViewInit(): void {
const ctx = this.canvas.nativeElement.getContext('2d');
ctx!.imageSmoothingEnabled = false;

this.ctx = new EditableContext2D(ctx!);
if (!ctx) {
throw new Error('CanvasRenderingContext2D is null');
}

ctx.imageSmoothingEnabled = false;

this.ctx = new EditableContext2D(ctx);
}

async loadImageFile(imageFile: File) {
if (!this.ctx) {
return;
}

const img = await loadImageFile(imageFile);
this.ctx.resetToImage(img);
this.zoomToFit();
this.activeColor = this.ctx.pick(0, 0);
this.colorCounts.next(this.ctx.count());
this.pixels.next(this.ctx.pixels());
this.hasImage = true;
}

set zoom(zoom: number) {
this.canvas.nativeElement.style.width = `${this.ctx.width * zoom}px`;
this.canvas.nativeElement.style.height = `${this.ctx.height * zoom}px`;
}

this.canvas.nativeElement.width = img.naturalWidth;
this.canvas.nativeElement.height = img.naturalHeight;
this.canvas.nativeElement.style.width = `${img.naturalWidth * SCALE}px`;
this.canvas.nativeElement.style.height = `${img.naturalHeight * SCALE}px`;
get zoom() {
return this.canvas.nativeElement.offsetWidth / this.ctx.width;
}

this.ctx!.ctx.fillStyle = '#ffffff';
this.ctx!.ctx.fillRect(0, 0, img.naturalWidth, img.naturalHeight);
this.ctx!.ctx.drawImage(img, 0, 0);
this.activeColor = this.ctx!.pick(0, 0);
this.colorCounts.next(this.ctx!.count());
this.pixels.next(this.ctx!.pixels());
this.hasImage = true;
zoomToFit() {
this.zoom = Math.trunc(
Math.max(
1,
Math.min(
window.innerWidth / this.ctx.width,
window.innerHeight / this.ctx.height,
MAX_SCALE
)
)
);
}

zoomIn() {
this.zoom = Math.min(MAX_SCALE, this.zoom + 1);
}

zoomOut() {
this.zoom = Math.max(1, this.zoom - 1);
}

onMouseDown(event: MouseEvent) {
if (!(event.buttons & 1)) {
return;
}

const x = Math.trunc(event.offsetX / SCALE);
const y = Math.trunc(event.offsetY / SCALE);
const x = Math.trunc(event.offsetX / this.zoom);
const y = Math.trunc(event.offsetY / this.zoom);

if (this.activeTool === Tool.DRAW) {
this.ctx?.draw(x, y, this.activeColor);
Expand Down
17 changes: 17 additions & 0 deletions src/app/canvas-editor/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ export type HexColor = `#${string}`;
export class EditableContext2D {
constructor(readonly ctx: CanvasRenderingContext2D) {}

get width() {
return this.ctx.canvas.width;
}

get height() {
return this.ctx.canvas.height;
}

resetToImage(img: HTMLImageElement) {
const { naturalHeight, naturalWidth } = img;
this.ctx.canvas.width = naturalWidth;
this.ctx.canvas.height = naturalHeight;
this.ctx.fillStyle = '#ffffff';
this.ctx.fillRect(0, 0, naturalWidth, naturalHeight);
this.ctx.drawImage(img, 0, 0);
}

count() {
const imageData = new EditableImageData(
this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
Expand Down
2 changes: 2 additions & 0 deletions src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ body {
height: 100%;
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
overflow: hidden;
overscroll-behavior: contain;
}

*[hidden] {
Expand Down

0 comments on commit 3430bf4

Please sign in to comment.