HTML Canvas: внутренняя тень для круговых фигур (планет)

Я намерен нарисовать полукруглые внутренние тени внутри круглых фигур, которые представляют планеты, движущиеся вокруг звезды (это часть образовательной программы, над которой я работаю).

После многих подходов это был тот, который почти работал для меня:

  1. Нарисуйте круглую форму (планету) и обведите ее большим кругом, в котором находится текущая тень.

Каждая планета в виде круга, который закрасит тень над ней2. Используя параметр композиции "ctx.globalCompositeOperation='source-atop';" Чтобы нарисовать больший круг, он будет рисовать только ту часть, которая перекрывает существующий контент:

Будет окрашена только область, которая перекрывает содержимое

Но проблема в том, что любая планета будет перекрывать любой теневой круг, поэтому, как вы можете видеть, когда планета перекрывает большую тень, она становится абсолютно темной.

Есть ли способ заставить его нарисовать область перекрытия определенного контента (фигуры)?

Или вы знаете лучший способ сделать это? Помните, я должен нарисовать тень под определенным углом от планеты к источнику света.

Заранее спасибо!

3 ответа

Решение

Попробуй позвонить clip метод (и соответствующие коды), прежде чем рисовать "тень" на планете, как это.

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>

Предварительно отрендерить тени.

Классное решение для вашей солнечной системы тень.

Некоторые устройства не любят рендеринг тени, и все маскирующие операции во время рендеринга отбирают у любого другого FX, который вы можете добавить.

Один из способов создания теней - это рендеринг тени для каждой планеты в начале. Маскируйте его так, чтобы он идеально подходил планете. Во время анимации просто нарисуйте планету, затем поверните теневое изображение лицом к солнцу и вызовите drawImage, чтобы получить тот же эффект, что и у вас, и во много раз быстрее.

пример

Функция createShadow создает собственное теневое изображение для планеты и добавляет его к объекту планеты как planet.shadow, Функция drawPlanet сначала рисует планету, а затем рисует тень над ней с нормальным source-over композитинга.

    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);

Я бы использовал несколько холстов. Я бы поддерживал "основной" холст и рисовал отдельные элементы на другом холсте, а затем смешивал их с основным холстом.

Этот вопрос содержит информацию о смешивании одного холста с другим: объединение двух или более элементов холста с некоторым смешиванием

Другие вопросы по тегам