1

Below is my preliminary Javascript code for making a analog clock. My main problem is I don't know how to clear the "previous second lines" on the clock surface:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <script>
        setInterval(timing, 1000);
        
        var canvas1 = document.createElement("canvas");
        canvas1.id = "canvas-1";
        document.body.appendChild(canvas1);
        canvas1.width = 500;
        canvas1.height = 500;
        canvas1.style.backgroundColor = "#3d3d3b";
        var radius = (canvas1.height/2) * 0.9;

        var ctx = canvas1.getContext("2d");  
        
        ctx.beginPath();
        ctx.arc(250,250,radius,0,2*Math.PI);
        ctx.fillStyle = "white";
        ctx.fill();
       
        ctx.beginPath();
        ctx.arc(250, 250, radius * 0.1, 0, 2 * Math.PI);
        ctx.fillStyle = '#333';
        ctx.fill();

        ctx.beginPath();
        ctx.lineWidth = radius * 0.05;
        ctx.stroke();
        ctx.font = "40px Georgia"
        ctx.textBaseline="middle";
        ctx.textAlign="center";
        for (i=1;i<13;i++){
        ctx.fillText(i.toString(), 250+(Math.sin(i*Math.PI/6)*radius*0.8), 250-Math.cos(i*Math.PI/6)*radius*0.8);
        }
        
        function timing(){
        
        const d = new Date();
        
        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.01;
        ctx.lineTo(250+(Math.sin(d.getSeconds()*Math.PI/30)*radius*0.85), 250-Math.cos(d.getSeconds()*Math.PI/30)*radius*0.85);        
        ctx.stroke(); 

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.03;
        ctx.lineTo(250+(Math.sin(d.getMinutes()*Math.PI/30)*radius*0.78), 250-Math.cos(d.getMinutes()*Math.PI/30)*radius*0.78);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.05;
        ctx.lineTo(250+(Math.sin(d.getHours()*Math.PI/6)*radius*0.7), 250-Math.cos(d.getHours()*Math.PI/6)*radius*0.7);
        ctx.stroke();
        }
    </script>
</body>
</html>

I have tried to use "ctx.globalCompositeOperation = "destination-over";", however not successful:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <script>
        setInterval(timing, 1000);
        
        var canvas1 = document.createElement("canvas");
        canvas1.id = "canvas-1";
        document.body.appendChild(canvas1);
        canvas1.width = 500;
        canvas1.height = 500;
        canvas1.style.backgroundColor = "#3d3d3b";
        var radius = (canvas1.height/2) * 0.9;

        var ctx = canvas1.getContext("2d"); 
        
        ctx.beginPath();
        ctx.arc(250,250,radius,0,2*Math.PI);
        ctx.fillStyle = "white";
        ctx.fill();
        
        ctx.beginPath();
        ctx.arc(250, 250, radius * 0.1, 0, 2 * Math.PI);
        ctx.fillStyle = '#333';
        ctx.fill();

        ctx.beginPath();
        ctx.lineWidth = radius * 0.05;
        ctx.stroke();
        ctx.font = "40px Georgia"
        ctx.textBaseline="middle";
        ctx.textAlign="center";
        for (i=1;i<13;i++){
        ctx.fillText(i.toString(), 250+(Math.sin(i*Math.PI/6)*radius*0.8), 250-Math.cos(i*Math.PI/6)*radius*0.8);
        }
        
        function timing(){
        const d = new Date();

            ctx.beginPath();
            ctx.arc(250,250,radius,0,2*Math.PI);
            ctx.fillStyle = "white";
            ctx.fill();
            ctx.globalCompositeOperation = "destination-over";
            ctx.beginPath();
            ctx.moveTo(250,250);
            ctx.lineTo(250+(Math.sin((d.getSeconds()-1)*Math.PI/30)*radius*0.85), 250-Math.cos((d.getSeconds()-1)*Math.PI/30)*radius*0.85);
            ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.01;
        ctx.lineTo(250+(Math.sin(d.getSeconds()*Math.PI/30)*radius*0.85), 250-Math.cos(d.getSeconds()*Math.PI/30)*radius*0.85);        
        ctx.stroke(); 

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.03;
        ctx.lineTo(250+(Math.sin(d.getMinutes()*Math.PI/30)*radius*0.78), 250-Math.cos(d.getMinutes()*Math.PI/30)*radius*0.78);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.05;
        ctx.lineTo(250+(Math.sin(d.getHours()*Math.PI/6)*radius*0.7), 250-Math.cos(d.getHours()*Math.PI/6)*radius*0.7);
        ctx.stroke();
        }
    </script>
</body>
</html>

Could you tell me how to clear these "previous second lines" by using globalCompositeOperation if such function can really do in my case? Thanks.

The reason i believe it is possible to do it through globalCompositeOperation, is because i had tried some test as below:

<html>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">
</canvas>
<button onclick="myFunction()">Click me</button>

<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.arc(50, 50, 50, 0, 2*Math.PI);
ctx.fillStyle = 'red';
ctx.fill();
ctx.beginPath();
ctx.moveTo(50,50);
ctx.lineTo(90,90);
ctx.stroke();

function myFunction() {
ctx.beginPath();
ctx.arc(50, 50, 50, 0, 2*Math.PI);
ctx.fillStyle = 'red';
ctx.fill();
ctx.globalCompositeOperation = "destination-over";
ctx.beginPath();
ctx.moveTo(50,50);
ctx.lineTo(90,90);
ctx.stroke();}

</script>

</body>
</html>

4
  • 2
    Not a canvas expert, but as I understand it: 1) redraw everything (ie wipe and recreate) or 2) use a transparent canvas overlay (2 or more canvas positioned on top of each other with transparent backgrounds). Don't forget it's not just the second hand that need to cleared, anything that moves (minute+hour hands). Commented Aug 25, 2022 at 7:21
  • setInterval(timing, 1000); will drift over time if Date wasn't used, or skip some updates and feel jittery if Date was. I would tighten up the loop. Commented Aug 27, 2022 at 14:17
  • @ggorlen requestAnimationFramework should solve the issues that you mention.... Commented Aug 28, 2022 at 15:18
  • Yup, but it'll also stop running if the user tabs out, so it's not necessarily the best solution. Commented Aug 28, 2022 at 16:21

3 Answers 3

1

The globalCompositeOperation property cannot really be used for this purpose.

You can however do this:

  • Create a second canvas element that overlays the first (using position: absolute). It is transparent, so the other canvas will be seen through it.
  • After drawing the background on the original canvas, switch the context (ctx) to the second canvas, so that the timing function will only deal with the overlayed canvas
  • In the timing function, start by clearing that overlay canvas

setInterval(timing, 1000);

// Create second canvas that will overlay the first
var canvas2 = document.createElement("canvas");
canvas2.width = 500;
canvas2.height = 500;
canvas2.style.position = "absolute";
document.body.appendChild(canvas2);

var canvas1 = document.createElement("canvas");
canvas1.id = "canvas-1";
document.body.appendChild(canvas1);
canvas1.width = 500;
canvas1.height = 500;
canvas1.style.backgroundColor = "#3d3d3b";
var radius = (canvas1.height/2) * 0.9;

var ctx = canvas1.getContext("2d");  

ctx.beginPath();
ctx.arc(250,250,radius,0,2*Math.PI);
ctx.fillStyle = "white";
ctx.fill();

ctx.beginPath();
ctx.arc(250, 250, radius * 0.1, 0, 2 * Math.PI);
ctx.fillStyle = '#333';
ctx.fill();

ctx.beginPath();
ctx.lineWidth = radius * 0.05;
ctx.stroke();
ctx.font = "40px Georgia"
ctx.textBaseline="middle";
ctx.textAlign="center";
for (i=1;i<13;i++){
    ctx.fillText(i.toString(), 250+(Math.sin(i*Math.PI/6)*radius*0.8), 250-Math.cos(i*Math.PI/6)*radius*0.8);
}

// Switch the context to the overlayed canvas
ctx = canvas2.getContext("2d");

function timing(){
    // Clear the second canvas (only)
    ctx.clearRect(0, 0, 500, 500);
    const d = new Date();

    ctx.beginPath();
    ctx.moveTo(250,250);
    ctx.lineWidth = radius*0.01;
    ctx.lineTo(250+(Math.sin(d.getSeconds()*Math.PI/30)*radius*0.85), 250-Math.cos(d.getSeconds()*Math.PI/30)*radius*0.85);        
    ctx.stroke(); 

    ctx.beginPath();
    ctx.moveTo(250,250);
    ctx.lineWidth = radius*0.03;
    ctx.lineTo(250+(Math.sin(d.getMinutes()*Math.PI/30)*radius*0.78), 250-Math.cos(d.getMinutes()*Math.PI/30)*radius*0.78);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(250,250);
    ctx.lineWidth = radius*0.05;
    ctx.lineTo(250+(Math.sin(d.getHours()*Math.PI/6)*radius*0.7), 250-Math.cos(d.getHours()*Math.PI/6)*radius*0.7);
    ctx.stroke();
}

Sign up to request clarification or add additional context in comments.

6 Comments

1)The second canvas is default to be in transparent? 2) In your comment you said "Switch the context to the overlayed canvas" and the code is "ctx = canvas2.getContext("2d");", is it meant to put the numbers, the inner circle and the circle circumstance from canvas1 to the canvas2? or is copied from canvas1 to canvas2? The numbers, inner circle..etc are still exist in canvas1. 3) I also see in the beginning of the timing function, you start by using the code "ctx.clearRect(0, 0, 500, 500);" to clear the second canvas, how the program can identify it is the first canvas or the second canvas?
1) All canvas elements are transparent by default (2) No, it's not a copy. That assignment ensures that when timing uses the ctx variable, it refers to the overlaid canvas (which initially has no drawing content). (3) So indeed, the ctx links to a one specific canvas, and that is what that assignment to ctx did.
But since all the numbers, inner circle..etc are now switched to the overlaid canvas2, and the timing function is start by clearing of the overlaid canvas2, then why we can still see the numbers, inner circle ..etc , those should be cleared...right? That's why i ask you those context is copied to or switch to the overlaid canvas...Thanks.
It is not like that. canvas1 is (still) the original background canvas that has the numbers, inner circle, ...etc. This drawing is made with ctx referencing canvas1. canvas2 is the overlay. It is empty when the timer hasn't ticked yet. ctx is redirected to point to that (empty, overlay) canvas2 . The timing function draws on that overlay canvas2 via ctx. Nothing is copied from one canvas to another in this process. ctx is just a "pointer". If it helps, you could also create a separate variable ctx2 and use that in timing.
I had just updated at the end of my question to add a simple demonstration on how to make the second hand disappear on the clock surface by using globalCompositeOpeartion. My logic is as also already shown in my program is to clear the previous line ((d.getSecond()-1) before the program go to d.getSecond(), however as also shown in my program, there is no second hand or hour hand can be seen...
|
1

You should probably be re-drawing the clockface for every new date that you render. I broke it down into individual pieces and used Promises but sure these were not strictly necessary.

(() => {

  let cnvs;
  let ctxt;
  let radius;

  const buildcanvas = () => new Promise((resolve, reject) => {
    cnvs = document.createElement("canvas");
    cnvs.id = "canvas-1";
    cnvs.width = 500;
    cnvs.height = 500;
    cnvs.style.backgroundColor = "#3d3d3b";
    document.body.appendChild(cnvs);

    resolve(true)
  });


  const buildclockface = () => new Promise((resolve, reject) => {
    radius = (cnvs.height / 2) * 0.9;
    ctxt = cnvs.getContext("2d");

    ctxt.beginPath();
    ctxt.arc(250, 250, radius, 0, 2 * Math.PI);
    ctxt.fillStyle = "white";
    ctxt.fill();

    ctxt.beginPath();
    ctxt.arc(250, 250, radius * 0.1, 0, 2 * Math.PI);
    ctxt.fillStyle = '#333';
    ctxt.fill();

    ctxt.beginPath();
    ctxt.lineWidth = radius * 0.05;
    ctxt.stroke();
    ctxt.font = "40px Georgia"
    ctxt.textBaseline = "middle";
    ctxt.textAlign = "center";

    for (i = 1; i < 13; i++) {
      ctxt.fillText(
        i.toString(),
        250 + (Math.sin(i * Math.PI / 6) * radius * 0.8),
        250 - (Math.cos(i * Math.PI / 6) * radius * 0.8)
      );
    }
    resolve(true)
  });

  const showtime = (d) => new Promise((resolve, reject) => {
    let d = new Date();
    buildclockface();
    secondhand(d);
    minutehand(d);
    hourhand(d);
  });

  const secondhand = (d) => {
    ctxt.beginPath();
    ctxt.moveTo(250, 250);
    ctxt.lineWidth = radius * 0.01;
    ctxt.lineTo(250 + (Math.sin(d.getSeconds() * Math.PI / 30) * radius * 0.85), 250 - Math.cos(d.getSeconds() * Math.PI / 30) * radius * 0.85);
    ctxt.stroke();
  }
  const minutehand = (d) => {
    ctxt.beginPath();
    ctxt.moveTo(250, 250);
    ctxt.lineWidth = radius * 0.03;
    ctxt.lineTo(250 + (Math.sin(d.getMinutes() * Math.PI / 30) * radius * 0.78), 250 - Math.cos(d.getMinutes() * Math.PI / 30) * radius * 0.78);
    ctxt.stroke();

  }
  const hourhand = (d) => {
    ctxt.beginPath();
    ctxt.moveTo(250, 250);
    ctxt.lineWidth = radius * 0.05;
    ctxt.lineTo(250 + (Math.sin(d.getHours() * Math.PI / 6) * radius * 0.7), 250 - Math.cos(d.getHours() * Math.PI / 6) * radius * 0.7);
    ctxt.stroke();
  }





  buildcanvas()
    .then(bool => setInterval(showtime, 1000))
    .catch(err => alert(err))

})();

5 Comments

Thanks. But can we done in a way that kept the background unchanged? or can this solution be solved by using globalCompositeOperation?
To be honest I'm not sure that it could be solved using globalCompositeOperation or not
I had just updated at the end of my question to add a simple demonstration on how to make the second hand disappear on the clock surface by using globalCompositeOpeartion. My logic is as also already shown in my program is to clear the previous line ((d.getSecond()-1) before the program go to d.getSecond(), however as also shown in my program, there is no second hand or hour hand can be seen...
I shall have a look at that as it bugged me for a while earlier.
I had just post an answer using globalCompositeOperation, however, it seems simply redraw the clock background each time maybe a better solution in this case....Thanks for all your help!
0

After some further research of the globalCompositeOperation, I find it is necessary to add another instruction to tell the program that the globalCompositeOperation restore to its previous state once the "previous second line" is cleared, therefore i further modified my program as below, and eventually it proof that using globalCompositeOperation can solve the problem. However, I had to admit that simply redraw the clock background each time should be better solution in this case.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Analog Clock-4 (using globalCompositeOperation)</title>
</head>
<body>
    <script>
        var canvas1 = document.createElement("canvas");
        canvas1.id = "canvas-1";
        document.body.appendChild(canvas1);
        canvas1.width = 500;
        canvas1.height = 500;
        canvas1.style.backgroundColor = "#3d3d3b";
        var radius = (canvas1.height/2) * 0.9;

        var ctx = canvas1.getContext("2d");

        setInterval(timing, 1000);        

        function timing(){
        const d = new Date();

        ctx.beginPath();
        ctx.arc(250,250,radius,0,2*Math.PI);
        ctx.fillStyle = "white";
        ctx.fill();

        ctx.beginPath();
        ctx.arc(250, 250, radius * 0.1, 0, 2 * Math.PI);
        ctx.fillStyle = '#333';
        ctx.fill();

        ctx.beginPath();
        ctx.lineWidth = radius * 0.05;
        ctx.stroke();
        ctx.font = "40px Georgia"
        ctx.textBaseline="middle";
        ctx.textAlign="center";
        for (i=1;i<13;i++){
        ctx.fillText(i.toString(), 250+(Math.sin(i*Math.PI/6)*radius*0.8), 250-Math.cos(i*Math.PI/6)*radius*0.8);
        }
        
        ctx.globalCompositeOperation = "destination-over";
        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineTo(250+(Math.sin((d.getSeconds()-1)*Math.PI/30)*radius*0.5), 250-Math.cos((d.getSeconds()-1)*Math.PI/30)*radius*0.5);
        ctx.stroke();

        ctx.globalCompositeOperation = "source-over";
        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.01;
        ctx.lineTo(250+(Math.sin(d.getSeconds()*Math.PI/30)*radius*0.85), 250-Math.cos(d.getSeconds()*Math.PI/30)*radius*0.85);        
        ctx.stroke(); 

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.03;
        ctx.lineTo(250+(Math.sin(d.getMinutes()*Math.PI/30)*radius*0.78), 250-Math.cos(d.getMinutes()*Math.PI/30)*radius*0.78);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(250,250);
        ctx.lineWidth = radius*0.05;
        ctx.lineTo(250+(Math.sin(d.getHours()*Math.PI/6)*radius*0.7), 250-Math.cos(d.getHours()*Math.PI/6)*radius*0.7);
        ctx.stroke();
        }
    </script>
</body>
</html>

3 Comments

Please read this meta post
@freedomn-m Hi, I am not very familiar with rules, you mean i should add some descriptions? Yes, i have just added...Thanks.
No worries - sometimes easier to reference that post than explain again. Answering your own question is fine.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.