Problems with the zoom functionality for images on canvas within Angular

Encountering a challenge with zooming in and out of an image displayed on canvas. The goal is to enable users to draw rectangles on the image, which is currently functioning well. However, implementing zoom functionality has presented the following issue:

When zooming in or out, the rectangles need to remain fixed relative to the image. While the rectangle stays in the same position, the part of the image contained within it changes as the image moves. How can I ensure that the rectangle covers the same part of the image even after zooming?

I am new to working with canvas and unsure about how to address this.

The problematic component is specified below:

tagger.component.html

<div class="row">
  <div class="text-image-content">
 
    <div class="column1">
        <div style="position: relative;">
            <div *ngFor="let drawItem of drawItems; let i = index">
                <input [ngStyle]="{'left':drawItem.x0 + 'px' , 'top': drawItem.y0 + 'px'}"
                    style="position: absolute; z-index: 999;" placeholder="Enter Object Name"
                    [(ngModel)]="drawItem.name" type="text">
                <input [ngStyle]="{'left':drawItem.x0 + 'px' , 'top': drawItem.y0 + 'px'}"
                    style="position: absolute; z-index: 999;" type="button" value="X" (click)="delete(i)">
            </div>
            
            <canvas #layer1 id="layer1" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
            <div class="row">
            </div>
        </div>
    </div>
    </div>
<button class="zoomBtn" (click)="zoomIn()">Zoom in</button>
<button class="zoomBtn" (click)="zoomOut()">Zoom out</button>
    <div class="column2">
        <div *ngFor="let drawItem of originalItems; let i = index">
            <div class="details">
              <div >Name: {{drawItem.name | titlecase}} </div>
               <div>({{drawItem.x0}}, {{drawItem.y0}} , {{drawItem.x0 + drawItem.x1}}, {{drawItem.y0 + drawItem.y1}})</div>
            </div>
        </div>
    </div>
</div>

tagger.component.ts

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

@Component({
    selector: "app-tagger",
    templateUrl: "./tagger.component.html",
    styleUrls: ["./tagger.component.scss"]
})
export class TaggerComponent implements OnInit {

    name = "Angular";
    constructor() { }
    drawItems = []
    originalItems = []
    count = 0
    @Input('CanvasHeight') CanvasHeight
    @Input('CanvasWidth') CanvasWidth
    @Output() selected = new EventEmitter();
    taggedItem = ""
    showInput: boolean = false;
    isMoving: boolean;
    public imgWidth: number;
    public uniX: number;
    public uniY: number;
    public uniX2: number;
    public uniY2: number;
    public initX: number;
    public initY: number;
    public imgHeight: number;
    public url: string;
    public image;
    public originalImageWidth;
    public originalImageHeight;
    public hRatio;
    public vRatio;
    public translatePos = {x: this.CanvasWidth / 2, y: this.CanvasHeight / 2};
    public scale = 1.0;
    public scaleMultiplier = 0.8;

    @ViewChild("layer1", { static: false }) layer1Canvas: ElementRef;
    private context: CanvasRenderingContext2D;
    private layer1CanvasElement: any;

  ngOnInit(){
    this.imageLoad();
  }

    delete(i) {
        console.log(i)
        this.drawItems.splice(i, 1);
        this.originalItems.splice(i,1);
        this.drawRect("red", 0, 0, 1);
    }

    imageLoad() {
      this.image = new Image();
      this.image.src = "https://i.ibb.co/12TJSNy/patio.jpg";
      this.image.onload = () => {
        console.log(this.CanvasWidth, this.CanvasHeight);
        console.log(this.image.width, this.image.height);
        this.originalImageWidth = this.image.width;
        this.originalImageHeight = this.image.height;
        this.image.width = this.CanvasWidth;
        this.image.height = this.CanvasHeight;
        this.hRatio =   this.originalImageWidth/this.CanvasWidth;
        this.vRatio =  this.originalImageHeight/this.CanvasHeight; 
        this.layer1CanvasElement = this.layer1Canvas.nativeElement;
        this.layer1CanvasElement.width = this.CanvasWidth;
        this.layer1CanvasElement.height = this.CanvasHeight;
        this.showImage();
      }
    }

    showImage() {
        this.count ++;
        this.layer1CanvasElement = this.layer1Canvas.nativeElement;
        this.context = this.layer1CanvasElement.getContext("2d");
        this.context.clearRect(0, 0, this.CanvasWidth, this.CanvasHeight);
        this.context.save();
        this.context.translate(this.translatePos.x, this.translatePos.y);
        this.context.scale(this.scale, this.scale);
        this.context.drawImage(this.image, 0,0, this.image.width, this.image.height);
        this.context.restore()
        let parent = this;
        if(this.count==1){
          this.layer1CanvasElement.addEventListener("mousedown", (e) => {
            this.isMoving = true
            this.initX = e.offsetX;
            this.initY = e.offsetY;
          });

        this.layer1CanvasElement.addEventListener("mouseup", (e) => {
            this.isMoving = false
            this.showInput = true
            this.drawItems.push({
                name: "",
                x0: this.initX,
                y0: this.initY,
                x1: this.uniX,
                y1: this.uniY
            });
            this.originalItems.push({
                name: "",
                x0: Math.ceil(this.initX * this.hRatio),
                y0: Math.ceil(this.initY * this.vRatio),
                x1: Math.ceil(this.uniX *this.hRatio),
                y1: Math.ceil(this.uniY * this.vRatio)
            });
            parent.drawRect("red", e.offsetX - this.initX, e.offsetY - this.initY, 0);
            this.uniX = undefined
            this.uniY = undefined
        });
      }
        

        this.layer1CanvasElement.addEventListener("mousemove", (e) => {
            if (this.isMoving) {
                parent.drawRect("red", e.offsetX - this.initX, e.offsetY - this.initY, 0);
            }
        });

        this.drawRect("red", 0, 0, 1);

    }


    drawRect(color = "black", height, width, flag) {
      if (this.uniX | flag) {
        this.context.clearRect(0, 0, this.CanvasWidth, this.CanvasHeight);
        this.context.save();
        this.context.translate(this.translatePos.x, this.translatePos.y);
        this.context.scale(this.scale, this.scale);
        this.context.drawImage(this.image, 0,0, this.image.width, this.image.height);
        this.context.restore()
      }
      // console.log(this.image.width, this.image.height)
      this.uniX = height
      this.uniY = width
      this.uniX2 = height
      this.uniY2 = width
      for (var i = 0; i < this.drawItems.length; i++) {
          this.context.beginPath();
          this.context.rect(
              this.drawItems[i].x0,
              this.drawItems[i].y0,
              this.drawItems[i].x1,
              this.drawItems[i].y1
          );
          this.context.lineWidth = 3;
          this.context.strokeStyle = color;
          this.context.stroke();
      }
    }

    zoomIn(){
      console.log("zooming in")
      this.scale /= this.scaleMultiplier;
      this.showImage();
    }
    zoomOut(){
      console.log("zooming out")
      this.scale *= this.scaleMultiplier;
      this.showImage();
      
    }
}

tagger.component.scsss

.button {
    color: transparent;
    cursor: pointer;
}

.column1 {
    float: left;
    width: 70%;
    padding: 10px;
    height: 300px; /* Should be removed. Only for demonstration */
}
.column2 {
    float: left;
    width: 25%;
    padding: 10px;
    height: 300px; /* Should be removed. Only for demonstration */
}

/* Clear floats after the columns */
.row:after {
    content: "";
    display: table;
    clear: both;
}

.details {
    margin: 10px;
}

.text-image-content{
border: 1px solid #979797;
    border-radius: 8px;
    padding: 0 10px;
    margin-left:20px;
    margin-right:20px;
    height: 300px;
    overflow-y: scroll;
    overflow-x: scroll;
    scrollbar-width: thin;
}

.zoomBtn{
  // height:10px;
}

Access the complete code at : https://stackblitz.com/edit/angular-tsotfa?file=src%2Fapp%2Ftagger%2Ftagger.component.ts

Thank you!

Answer №1

To adjust the zoom level in your Zoomin or Zoomout function, simply modify the coordinates of the bounding boxes within this.drawItems. Making these changes will help you achieve the desired outcome.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Activating a hyperlink within a Div element by clicking on the Div itself

In my card element, I have a link that, when clicked, opens a modal pop-up containing all the necessary project information. I am currently attempting to trigger a click event on that link whenever any part of the div is clicked. This task is for a school ...

Ensure that selecting one checkbox does not automatically select the entire group of checkboxes

Here is the code I'm using to populate a list of checkboxes. <label class="checkbox-inline" ng-repeat="item in vm.ItemList track by item.id"> <input type="checkbox" name="item_{{item.id}}" ng-value="{{item.id}}" ng-model="vm.selectedItem" /& ...

Struggling to map a JSON file in a NextJS project using Typescript

I'm a beginner in JS and I'm currently struggling to figure out how to display the data from a json file as HTML elements. Whenever I run my code on the development server, I encounter the error message: "props.books.map is not a function&q ...

A guide to using JavaScript to create a button that can dynamically change stylesheets

Just a heads up, I'm not using classes in this particular scenario. Can't seem to find the answer to this exact question. With javascript, how can I code a button to switch stylesheets every time it's clicked? I've experimented with d ...

PhantomJS encountered a TypeError when trying to access a non-existent object during the evaluation of 'document.getElementById()'

Recently, I delved into the world of PhantomJS and its ability to extract data from websites through JavaScript. This process is commonly referred to as web scraping. My goal was to retrieve the text content of an element by its ID. However, I encountered ...

Can you rely on a specific order when gathering reactions in a discord.js bot?

Imagine a scenario where a bot is collecting reactions to represent event registrations. To prevent any potential race conditions, I have secured the underlying data structure with a mutex. However, the issue of priority still remains unresolved as user # ...

Troubleshooting issues with applying styles in Vue framework when configured with webpack

I'm facing an issue with setting up the Vue framework using webpack. Specifically, I'm having trouble with styles not being applied when included in the <style> tag within single file components. Despite following several tutorials on this ...

Adjusting the dimensions of the cropper for optimal image cropping

I am currently working on integrating an image cropper component into my project, using the react-cropper package. However, I am facing a challenge in defining a fixed width and height for the cropper box such as "width:200px; height:300px;" impo ...

Having difficulty manually concealing the launch image on the physical device

When testing my trigger.io app on the Android simulator or my Nexus phone, I can manually hide the launch image through code successfully. However, when running the app on the iOS simulator, the launch image remains visible. Additionally, when debugging di ...

The scripts are mistakenly interpreted as stylesheets, but are actually being transferred with the MIME type text/html

Encountering two console warnings while using Chrome: Resource interpreted as Stylesheet but transferred with MIME type text/html: "http://domain/". domain/:11 Resource interpreted as Stylesheet but transferred with MIME type text/html: "http://domain/". ...

Inheriting Angular components: How can the life cycle hooks of a parent component be triggered?

So I'm working with BaseComponent and a number of child components that extend it: export class Child1Component extends BaseComponent implements OnInit, AfterViewInit In the case of Child1Component, there is no explicit call to super.ngAfterViewInit ...

How to resolve the error of "Cannot GET /api/courses/1"

const express = require('express'); const app = express(); app.get('/',(req,res) =>{ // viewable at localhost:3000 res.send('hello world'); }); app.get('/api/courses',(req,res) =>{ // shown on ...

Can ajax requests be made without a web browser?

Currently, I am in the process of developing JavaScript tests using mocha and chutzpah, which means conducting all my tests without a browser. However, I have encountered an issue where all my AJAX calls are resulting in empty strings. Even when using the ...

Obtain an array containing only the specific values of each object

Currently, I have the following code snippet: <db.collection>.find({ id : { $exists: true } }) This query retrieves all documents containing an id property. However, I am interested in transforming this result into an array that contains only the va ...

Having Trouble Styling Radio Buttons with CSS

Hello, I'm facing an issue with hiding the radio button and replacing it with an image. I was successful in doing this for one set of radio buttons, but the second set in another row is not working properly. Additionally, when a radio button from the ...

Ways to integrate user input into the header of an Angular HTTP post method

I need help figuring out how to incorporate user input into the header of a post method I am working with. I understand that some kind of binding is necessary, but I'm struggling to implement it in this case. Currently, I have a variable called postDa ...

Troubleshooting AJAX issues in Firefox with window.location.assign function

Here is my AJAX POST request that sends serialized form data to the server: // Handle form submission. $('#evaluationform').on('submit', function(e){ e.preventDefault(); ajaxObject = { url: $("#evaluationform").attr("a ...

Discover the path with the power of JavaScript

Imagine I am including a JavaScript file in this manner: <script type="text/javascript" src="http://foo.com/script.js?id=120#foo"></script> Would it be possible to access the GET or hash parameters passed through this? Currently, I achieve t ...

Having trouble getting Angular2 tutorial animations to work in IE11?

After successfully setting up a local development environment using the Angular2 tutorial with the quick start seed [1], I decided to experiment with animations. Following a modified version of an example from [2], my app.component.ts now toggles backgroun ...

What is the best way to transfer an integer from my main application to a separate JavaScript file?

Currently, I am developing a complex code using React Bootstrap and focusing on creating a Dropdown list that fetches data from the backend database. <Dropdown> <Dropdown.Toggle variant="success" id="dropdown-basic"></D ...