Tips for creating a scale animation using HTML5 Canvas

I am currently developing a canvas whiteboard tool and I have reached the stage where I am focusing on implementing the Zoom In and Zoom Out feature.

While the functionality is working fine, I would like to enhance it with smooth animations for scaling. However, the current scaling happens too quickly that the effect is not noticeable. Whenever I click on the + or - button, the scaling occurs instantly.

let scaleAmount = 1.00;

function scaleUpdate()
{
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(scaleAmount, scaleAmount)   
    
    scaleSize.innerText = parseInt(parseFloat(scaleAmount.toFixed(2))*100)+"%";

    stageUpdate();  
   
}

function scale(direction)
{
    let scaleSize = document.getElementById("scaleSize");
    
    if(direction=="zoomIn")
    { 
        for(let i = 0; i<10; i++)
        {
            scaleAmount += 0.01;
            scaleAmount = parseFloat(scaleAmount.toFixed(2));
            dx += scaleAmount;
            dy += scaleAmount;   
            
            scaleUpdate();
        }
    }
    if(direction=="zoomOut")
    {
        for(let i = 0; i<10; i++)
        {
            scaleAmount -=0.01;
            scaleAmount = parseFloat(scaleAmount.toFixed(2));
            dx -= scaleAmount;
            dy -= scaleAmount;
            scaleUpdate();
        }
    }
}

dx and dy represent screen offsets.

Can anyone assist me in addressing this issue?

Thank you, Frank

Answer №1

When updating visual properties such as scale within a for-loop, the changes happen almost instantaneously - not literally all at once but certainly much faster than the human eye can perceive. The screen does not refresh with each iteration of the loop; instead, it appears to update only before and after calling the scale() function.

To make these changes visible over time, you must implement them gradually. For instance, starting at time 1.0, set the scale to 1.0, at 1.1, set it to 1.01, at 1.2, set it to 1.02, and so on until reaching the desired scale. This can be achieved using either the setInterval() or requestAnimationFrame() functions. setInterval requires two arguments: the function to call and the time (in milliseconds) at which to call this function. On the other hand, requestAnimationFrame attempts to call the specified function at the display's refresh rate - approximately 60 times per second on a 60Hz display.

To optimize your example using the requestAnimationFrame function:

let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");

document.getElementById("scaleUp").onclick = () => {
  cancelAnimationFrame(animationID);
  scale("zoomIn");
}
document.getElementById("scaleDown").onclick = () => {
  cancelAnimationFrame(animationID);
  scale("zoomOut");
}

let scaleAmount = 1.0;
let animationID;

function scale(direction) {
  let animationReady = false;
  let targetScale;
  if (direction == "zoomIn") {
    scaleAmount += 0.01;
    targetScale = 2;
    animationReady = scaleAmount >= targetScale;
  }
  if (direction == "zoomOut") {
    scaleAmount -= 0.01;
    targetScale = 1;
    animationReady = scaleAmount <= targetScale;
  }

  if (animationReady) {
    scaleAmount = targetScale;
  } else {
    animationID = requestAnimationFrame(() => {
      scale(direction);
    });
  }
  stageUpdate();
}

function stageUpdate() {
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.scale(scaleAmount, scaleAmount);
  ctx.beginPath();
  ctx.arc(0, 0, 45, 0, 2 * Math.PI);
  ctx.closePath();
  ctx.stroke();
}

stageUpdate();
<button id="scaleUp" type="button">Scale +</button><button id="scaleDown" type="button">Scale -</button><br><canvas id="canvas" width="320" height="200"></canvas>

Answer №2

@uniqueUser Your insight was incredibly valuable, much appreciated.

After incorporating your suggestions, my final code now appears as follows:

let scaleAmount = 1.00;
let scaleAnimation;
let animationReady = false;
let scalePercent =  parseInt(scaleSize.innerText.replace("%",""));
function scaleUpdate()
{
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(scaleAmount, scaleAmount)   
    
    scaleSize.innerText = parseInt(parseFloat(scaleAmount.toFixed(1))*100)+"%";

    stageUpdate();  
   
}

function scale(direction)
{
    
    
    let targetScale ;

    if(direction=="zoomIn")
    { 
            targetScale = scalePercent / 100 + 0.1;
            
            scaleAmount += 0.01;
        
            animationReady = scaleAmount > targetScale;
            targetScale= parseFloat(targetScale.toFixed(3));
            scaleAmount = parseFloat(scaleAmount.toFixed(3));
            dx += scaleAmount;
            dy += scaleAmount;   
    }
    if(direction=="zoomOut")
    {
            targetScale = scalePercent / 100 - 0.1;

            scaleAmount -= 0.01;
           
            animationReady = scaleAmount < targetScale;
            targetScale= parseFloat(targetScale.toFixed(3));
            scaleAmount = parseFloat(scaleAmount.toFixed(3));
            dx -= scaleAmount;
            dy -= scaleAmount;
    }
 
    if (animationReady) {
        scaleAmount = targetScale;
        scalePercent =  parseInt(scaleSize.innerText.replace("%",""));
    } else {
        scaleAnimation = requestAnimationFrame(() => {
            scaleUpdate();
            scale(direction);
         });
    }
}

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

Locate the parent element that has the smallest amount of space taken up by its child elements

My goal is to determine which container, among several <divs>, each containing multiple child <divs> of varying sizes, has the smallest amount of space covered by the child elements. <div class="container" id="first"> ...

Leveraging reframe.js to enhance the functionality of HTML5 video playback

I'm struggling with using the reframe.js plugin on a page that includes HTML5 embedded video. When I try to use my own video file from the same folder, it doesn't work as expected. While everything seems to be working in terms of the page loadin ...

When trying to validate an HTML form using AJAX, jQuery, and JavaScript, the validation may not

Here is a high-level overview of what happens: The following code functions correctly... <div id='showme'></div> <div id='theform'> <form ...> <input required ... <input required ... <inpu ...

How can I capture the 'ended' event from an HTML5 video tag using Ember in a controller?

I am having an issue with an hbs file that contains an html5 video tag: <video id="externalVideo" controls loop> <source src="../assets/videos/test.mp4" type="video/mp4"> Your browser does not support HTML5 video. </video> I am ...

Managing the scrolling direction horizontally with waypoints.js

As I work on creating a custom wizard form with waypoints, I've encountered an interesting issue that has left me puzzled. In my sample CODEPEN, you can see two pages of the wizard process to better understand the problem. Upon clicking the forward ...

Dependency management in ReactJS components

I am grappling with the optimal structure for a React component that is composed of other components. Let's look at the first example: <ColorSelect id="color" label={this.state.selectLabel} trigger={{ color: "lime", text: "Lime"}} onPropagateCli ...

Ways to extract a specific element from a webpage's body section

When utilizing node-fetch to retrieve the body of a site, I follow this process: import fetch from 'node-fetch'; (async () => { const response = await fetch('link'); const body = await response.text(); console.log(body) ...

Oops! Hardhat Test Error "Error: Virtual Machine Exception occurred while processing transaction: reverted with reason 'Please deposit additional funds'."

Encountering an issue with the following error message: Error: VM Exception while processing transaction: reverted with reason string 'deposit more' in the Hardhat Test.js file Test.js -> it("should be able to withdraw if no one appl ...

Fixed Positioning Div to Stay at the Top while Scrolling

Currently, I have successfully implemented the functionality to stick the div to the top once it scrolls down by 320px. However, I am curious if there is an alternative approach to achieving this effect. Below is the code snippet I am using: jQuery(functi ...

How can you transfer data from a jQuery function to a designated div element?

I'm struggling to transfer data from a function to a specific div, but I can't seem to make it work. I'm in the process of creating a gallery viewer and all I want is to pass the counter variable, which I use to display images, and the total ...

Utilizing numerous X-axis data points in highcharts

I'm working with a line graph that dips straight down, like starting at (1, 100) and dropping to (1,0). The issue I'm facing is that Highcharts () only displays information for one of the points. Is there a way to make it show information for bot ...

The test is failing to execute the service mock promise due to an issue with the `

A problem has arisen while creating a mock for the BoardService. It appears that the .then function is not executing in the controller during testing, even though it works perfectly fine in the live application. Below is the test snippet: beforeEach(inje ...

When I attempt to connect to my local MongoDB database, including a specific port in the URI is preventing the connection from being

While testing a connection to a local DB using mongoose and mongodb, I encountered an issue. Whenever I include a port number in the URI passed to mongoose.connect(), I receive a connection refused error. async function connectDB() { const db = await m ...

Can someone provide a method to access the namespace of an Angular controller when utilizing the "Controller As" format?

Imagine you have an AngularJS ngController directive set up like this: <div ng-controller="SomeCtrl as herpderp">…</div> Is there a way to extract the namespace ("herpderp") from within the SomeCtrl controller itself? This could be useful f ...

Correct validation is not achieved when the "!" symbol is used in the matches function

I am working on a React and Next.js project where I am using Formik for handling forms and Yup for validations. One specific input field requires some validations to be performed. This field must be required, so if the user does not enter any information, ...

Tips for dynamically adding an HTML element to index.html using JavaScript

How can I use JavaScript to insert cards into a container or display them in flex? Currently, the cards are not displaying as desired. I attempted to insert data into the note class using insertAdjacentHTML in JavaScript, but struggled to place them in the ...

Error in Visual Studio with Angular 2 build: 'Promise' name not found

I recently started exploring Angular2 and followed the instructions provided in this quickstart guide: https://angular.io/guide/quickstart Everything seems to be working well after running npm install, but now I want to work on it within Visual Studio usi ...

Jest tutorial: mocking constructor in a sub third-party attribute

Our express application uses a third-party module called winston for logging purposes. const express = require('express'); const app = express(); const { createLogger, transports } = require('winston'); const port = process.env.PORT | ...

Sending data through the backbone form?

Whenever the submit button is clicked, a post request should be made to the server with input data and it will return a JSON object. I am unsure where to define the success function and how to receive the response object. Is this the correct way to call a ...

Performing tasks when a component is fully loaded in Vue.js Router

I am currently working on a project involving a single-page application built with Vue.js and its official router. For this project, I have set up a menu and a separate component (.vue file) for each section that is loaded using the router. Inside every c ...