htmlcanvasglobalcompositeoperation

Place object between another two


I'm new to canvas and I wanted to move my DOM (HTML+JS) animation to canvas. I tried to mimic the solar system animation in Basic animations - Web API Interfaces | MDN and I managed to do it. Fiddle.

I have a problem regarding the globalCompositeOperation. In the animation, earth is casting a shadow (low opacity rectangle), and when the moon is orbiting this dark area, moon should be under the shadow rectangle. If I use

context.globalCompositeOperation = 'destination-over';

at the top of draw function, I get the intended result (fiddle), but then moon and the earth rotates beneath the orbit lines. I don't want that. I want both the earth and the moon to be above the orbit lines, and moon to be beneath the shadow rectangle. Is there a way to do that? If not, I'm gonna have to get rid of the shadow.

I didn't use context.save() and context.restore() in my code, though they are used in the original animation. I'm not familiar with them. Might be related to my problem...

var sun = {
  radius	: 80,
  x		: 0,
  y		: 0,
};
var earth = {
  radius	: 20,
  x		: sun.radius + 80,
  y		: 0,
  angle	: 0,
};
var moon = {
  radius	: 8,
  x		: 50,
  y		: 0,
  angle	: 0,
};

function draw() {
  var canvas	= document.getElementById("canvas");
  var context	= canvas.getContext("2d");

  context.resetTransform();
  context.clearRect(0, 0, canvas.width, canvas.height)
  context.translate(canvas.width/2, canvas.height/2);

  // sun
  context.beginPath();
  context.arc(0, 0, sun.radius, 0, 360 * Math.PI/180, false);
  context.fillStyle = "yellow";
  context.fill();

  // sun orbit
  context.beginPath();
  context.arc(0, 0, earth.x, 0, Math.PI*2, false);
  context.strokeStyle = "rgba(180,150,0,.25)";
  context.stroke();

  // earth
  earth.angle += .2;
  if (earth.angle == 360) {
    earth.angle = 0;
  }
  context.globalCompositeOperation = 'source-over'; // earth is above the sun and its orbit
  context.rotate(earth.angle * Math.PI/180);
  context.translate(earth.x, 0);
  context.beginPath();
  context.arc(0, 0, earth.radius, 0, 360*(Math.PI/180), false);
  context.fillStyle = "royalblue";
  context.fill();
  context.globalCompositeOperation = 'destination-over'; // earth's orbit and shadow are below the earth

  // earth shadow
  context.fillStyle = "rgba(0,0,0,0.1)";
  context.fillRect(0, -earth.radius, earth.radius*16, earth.radius*2);

  // earth orbit
  context.beginPath();
  context.arc(0, 0, moon.x, 0, Math.PI*2, false);
  context.strokeStyle = "rgba(0,0,200,.2)";
  context.stroke();

  // moon
  moon.angle += 1;
  if (moon.angle == 360) {
    moon.angle = 0;
  }
  context.globalCompositeOperation = 'source-over'; // moon is above the earth's orbit and shadow
  context.rotate(moon.angle * Math.PI/180);
  context.translate(moon.x, 0);
  context.beginPath();
  context.arc(0, 0, moon.radius, 0, 360 * Math.PI/180, false);
  context.fillStyle = "silver";
  context.fill();

  requestAnimationFrame(draw);
}

draw();
html {
  background: rgb(230,230,230);
}
canvas {
  background: white;
  border: 1px solid rgba(0,0,0,.2);
  box-shadow: 0 0 5px rgba(0,0,0,.1);
}
<canvas id="canvas" width="640" height="480"></canvas>


Solution

  • You just need to change a little on the orders of things in the code - composite modes are really not needed in this case:

    In this update I did not recalculate the rotation etc. but used save/restore.

    var sun = {
        radius	: 80,
        x		: 0,
        y		: 0,
    };
    var earth = {
        radius	: 20,
        x		: sun.radius + 80,
        y		: 0,
        angle	: 0,
    };
    var moon = {
        radius	: 8,
        x		: 50,
        y		: 0,
        angle	: 0,
    };
    
    var canvas	= document.getElementById("canvas");
    var context	= canvas.getContext("2d");
    
    context.setTransform(1,0,0,1,canvas.width/2, canvas.height/2);
    
    // sun
    context.beginPath();
    context.arc(0, 0, sun.radius, 0, 360 * Math.PI/180, false);
    context.fillStyle = "yellow";
    context.fill();
    
    // sun orbit
    context.beginPath();
    context.arc(0, 0, earth.x, 0, Math.PI*2, false);
    context.strokeStyle = "rgba(180,150,0,.25)";
    context.stroke();
    
    canvas.style.backgroundImage = "url(" + canvas.toDataURL() + ")";
    
    function draw() {
    
        context.setTransform(1,0,0,1,0,0);    
        context.clearRect(0, 0, canvas.width, canvas.height)
        context.setTransform(1,0,0,1,canvas.width/2, canvas.height/2);
        
        // earth
        earth.angle += .2;
        if (earth.angle == 360) {
            earth.angle = 0;
        }
        context.rotate(earth.angle * Math.PI/180);
        context.translate(earth.x, 0);
    
            // earth orbit
        context.beginPath();
        context.arc(0, 0, moon.x, 0, Math.PI*2, false);
        context.strokeStyle = "rgba(0,0,200,.2)";
        context.stroke();
        
        // moon
        context.save();
        moon.angle += 1;
        if (moon.angle == 360) {
            moon.angle = 0;
        }
        context.rotate(moon.angle * Math.PI/180);
        context.translate(moon.x, 0);
        context.beginPath();
        context.arc(0, 0, moon.radius, 0, 360 * Math.PI/180, false);
        context.fillStyle = "silver";
        context.fill();
        context.restore();
    
        // earth shadow
        context.fillStyle = "rgba(0,0,0,0.1)";
        context.fillRect(0, -earth.radius, earth.radius*16, earth.radius*2);
    
        context.beginPath();
        context.arc(0, 0, earth.radius, 0, 360*(Math.PI/180), false);
        context.fillStyle = "royalblue";
        context.fill();
        
        
        requestAnimationFrame(draw);
    }
    
    draw();
    html {
        background: rgb(230,230,230);
    }
    canvas {
        background: white;
        border: 1px solid rgba(0,0,0,.2);
        box-shadow: 0 0 5px rgba(0,0,0,.1);
    }
        <canvas id="canvas" width="640" height="480"></canvas>