1

my intention is to draw semi-circular inner shadows inside circular shapes that represent planets moving around a star (this is part of an educational program I'm working on).

After many approaches, this was the one that almost worked for me:

  1. Draw a circular shape (planet) and over it, stroke a larger circle that contains the actual shadow.

Each planet as a circle that will paint the shadow over it 2. Using the composition option "ctx.globalCompositeOperation='source-atop';" to draw the bigger circle it will only paint the portion that overlaps the existing content:

Only the area that overlaps content will be painted

But the problem is that any planet will overlap any shadow circle, so, as you can see, when a planet overlaps the bigger shadow it turns totally dark.

Is there any way to make it draw the overlap area of a specific content (shape)?

Or, do you know a better way to do this? Remember I must paint the shadow in the specific angle from the planet to the light source.

Thanks in advance!

Eduardo G.R.
  • 377
  • 3
  • 18

3 Answers3

3

Pre render the shadows.

Cool solution for you solar system shadow.

Some devices do not like rendering the shadow, and all the masking ops during rendering will take away from any other FX you may add.

One way to do the shadows is to render a shadow for each planet at the start. Mask it so it fits the planet perfectly. During animation just draw the planet, then rotate the shadow image to face the sun and call drawImage to get the same effect as you had and many times quicker.

Example

The function createShadow creates a custom shadow image for a planet and adds it to the planet object as planet.shadow. The function drawPlanet draws the planet first and then draws the shadow over it with normal source-over compositing.

    var canvas = document.createElement("canvas");
    canvas.width = canvas.height = 1024;
    var ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);
    
    const shadowImageSafeEdge = 2; // pixel safe border around shadow image
    const shadowBlur = 0.8; // fraction of planet radius
    var sun = {
        x : canvas.width /2,
        y : canvas.height / 2,
        radius : 80,
        color : "yellow",
    }
    var sunGrad = ctx.createRadialGradient(0, 0, sun.radius/4, 0, 0, sun.radius);
    sunGrad.addColorStop(0,"#FF7");
    sunGrad.addColorStop(0.6,"#FF4");
    sunGrad.addColorStop(0.8,"#FF0");
    sunGrad.addColorStop(1,"#DC0");
    sun.color = sunGrad;

    function rInt(min,max){
        return Math.floor((max-min) * Math.random() + min);
    }
    function randCol(hue){
        var col = "hsl(";
        col += Math.floor(hue + rInt(-30,30) + 360) % 360;
        col += ",";
        col += Math.floor(80 + rInt(-20,20) + 100) % 100;
        col += "%,";
        col += Math.floor(50 + rInt(-10,10) + 100) % 100;
        col += "%)";
        return col;
    }
    // creates a planet at orbit distance from sun    
    function createPlanet(orbit){
        var planet = {
            radius : Math.random() * 20 + 5,
            orbitDist : orbit,  // dist from sun
            orbitPos : Math.random() * Math.PI * 2,
            shadow : null,
        }
        planet.color = randCol(rInt(280, 360));
        planet.shadow = createShadow(planet);
        return planet;        
    }
    // creates a shadow image that fits the planet
    function createShadow(planet){    
        var r = planet.radius;
        var s = shadowImageSafeEdge;
        var planetShadow = document.createElement("canvas");
        planetShadow.width = planetShadow.height = r * s + s * 2; // a little room to stop hard edge if zooming
        var ctx = planetShadow.ctx = planetShadow.getContext("2d");
        ctx.shadowBlur = r * shadowBlur  ;
        ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
        ctx.lineWidth = r * 2 - r * (1 - shadowBlur / 2);
        ctx.strokeStyle = ctx.shadowColor = "rgba(0,0,0,1)";
        ctx.beginPath();
        ctx.arc(-planet.orbitDist - r,r + s, planet.orbitDist + r * 2 + r * (shadowBlur /0.85) + s, 0, Math.PI * 2);
        ctx.stroke();
        ctx.stroke();
        ctx.stroke();
        ctx.shadowColor = "rgba(0,0,0,0)";
        ctx.globalCompositeOperation = "destination-in";
        ctx.beginPath();
        ctx.arc(r + s, r + s, r, 0, Math.PI * 2);  // sun will be along x axis
        ctx.fill();
        ctx.globalCompositeOperation = "source-over";
        return planetShadow;
    }

    // draws the planet and the shadow
    function drawPlanet(planet){
         var xdx = Math.cos(planet.orbitPos); 
         var xdy = Math.sin(planet.orbitPos);
         var x = xdx * planet.orbitDist + sun.x;
         var y = xdy * planet.orbitDist + sun.y;
         ctx.setTransform(1,0,0,1,x,y);
         ctx.fillStyle = planet.color;
         ctx.beginPath();
         ctx.arc(0,0,planet.radius,0,Math.PI * 2);
         ctx.fill();

         // set transform so that shadow faces away from the sun
         ctx.globalAlpha = 0.8;
         ctx.setTransform(xdx,xdy,-xdy,xdx,x,y);
         ctx.drawImage(planet.shadow,-planet.radius - 2,-planet.radius - 2);
         ctx.globalAlpha =1;
     }
     // let you guess what this function does
     function drawSun(){
         ctx.fillStyle = sun.color;
         ctx.setTransform(1,0,0,1,sun.x,sun.y);
         ctx.beginPath();
         ctx.arc(0,0,sun.radius,0,Math.PI * 2);
         ctx.fill();
     }
     // array of planets and create them
     var planets = [];
     (function(){
        var i = 10;
        while(i-- >1){
            planets.push(
                createPlanet(
                   rInt( 60 + i * 40,i * 40 + 100)
                )
            );
        }  
     }());
     // gradient for background
     var backGrad = ctx.createRadialGradient(512, 512, sun.radius, 512, 512, Math.sqrt(512 * 512 * 2));
     backGrad.addColorStop(0,"#B9E");
     backGrad.addColorStop(0.025,"#96A");
     backGrad.addColorStop(1,"#624");

     // main render loop
     function render(time){
         ctx.setTransform(1,0,0,1,0,0); // reset transform
         ctx.fillStyle = backGrad;   
         ctx.fillRect(0,0,1024,1024);  // clear
         drawSun();  
         for(var i = 0; i < planets.length; i++){ // draw all planets
             planets[i].orbitPos += Math.sqrt(10 / Math.pow(planets[i].orbitDist, 2));
             drawPlanet(planets[i]);
         }
         requestAnimationFrame(render);
     }
     requestAnimationFrame(render);
Community
  • 1
  • 1
Blindman67
  • 51,134
  • 11
  • 73
  • 136
2

Try to call clip method (and related codes) before drawing "shadow" onto planet like this.

const ctx = canvas.getContext("2d");

//draw planet
ctx.beginPath();
ctx.arc(100, 100, 80, 0, Math.PI*2);
ctx.fillStyle = "aqua";
ctx.fill();
//save non-clipped state.
ctx.save();
//clip range by planet area.
ctx.clip();
//draw shadow
ctx.beginPath();
ctx.arc(200, 200, 200, 0, Math.PI*2);
ctx.lineWidth = 100;
ctx.stroke();
//dispose clip range.
ctx.restore();
<canvas id="canvas" width="200" height="200"></canvas>
defghi1977
  • 5,081
  • 1
  • 30
  • 29
0

I would use several canvas. I would maintain a "main" canvas and draw individual elements to other canvas, and then blend them into the main canvas.

This question has information on blending one canvas to another: Combining two or more Canvas elements with some sort of blending

Community
  • 1
  • 1
Nelson Teixeira
  • 6,297
  • 5
  • 36
  • 73