【问题标题】:How to shade the circle in canvas如何在画布中为圆圈着色
【发布时间】:2016-02-21 03:26:11
【问题描述】:

我正在与HTML5canvas 一起工作。我已经画了一个 2D 圆圈。现在我想用颜色为圆圈着色。但是阴影看起来像一个 3D 圆圈。这可以用画布吗?谢谢。

【问题讨论】:

  • 你在使用 WebGL 吗?
  • 不,不使用 WebGL。这可能吗?

标签: html canvas


【解决方案1】:

假烟雾和镜子

在球体上假装一盏灯。我猜它是一个球体,正如你所说的圆圈,你可能指的是一个甜甜圈。这种技术也适用于甜甜圈。

照明也是如此。

Phong 阴影

最基本的光照模型是 Phong(凭记忆)。它使用入射光线与表面法线(从表面以 90 度射出的线)之间的角度。反射光量是该角度乘以光强度的余弦值。

球体很容易

由于球体是对称的,这允许我们使用径向渐变来为球体上的每个像素应用值,并且对于直接在头顶上的光线的球体,这可以轻松生成完美的 phong 阴影球体。

执行此操作的代码。 x,y 是球体的中心,r 是半径。当您从球体中心移出时,光线和表面法线之间的角度很容易计算。它从零开始,在 Math.PI/2 (90deg) 结束。所以反射值是那个角度的余弦值。

    var grd = ctx.createRadialGradient(x,y,0,x,y,r);
    var step = (Math.PI/2)/r;
    for(var i = 0; i < (Math.PI/2); i += step){
       var c = "" + Math.floor(Math.max(0,255 * Math.abs(Math.cos(i)));
       grd.addColorStop(i/(Math.PI/2),"rgba("+c+","+c+","+c+","1)");
    }

该代码创建了一个渐变以适应圆。

荷马食物的模组

要做一个甜甜圈你需要修改 i.甜甜圈有一个内半径和外半径 (r1, r2) 所以在 for 循环内修改 i

 var ii = (i/(Math.PI/2)); // normalise i
 ii *= r2; // scale to outer edge
 ii = ((r1+r2)/2)-ii; // get distance from center line
 ii = ii / ((r2-r1)/2); // normalise to half the width;
 ii = ii * Math.PI * (1/2); // scale to get the surface norm on the donut.
 // use ii as the surface normal to calculate refelected light
 var c = "" + Math.floor(Math.max(0,255 * Math.abs(Math.cos(ii)));

Phong 阴影很烂

通过 phong 着色会浪费大量时间并且不会这样做。这也不允许灯光偏离中心,甚至部分位于球体后面。

我们需要添加偏离中心光的功能。幸运的是径向渐变可以偏移

  var grd = ctx.createRadialGradient(x,y,0,x,y,r);

前 3 个数字是渐变的起始圆,可以放置在任何位置。问题是当我们移动起始位置时,phong 着色模型会崩溃。为了解决这个问题,有一些烟雾和镜子可以让眼睛相信大脑想要的东西。

我们根据光线与中心的距离来调整径向渐变上每个色标的衰减、亮度、扩散和角度。

镜面高光

这稍微改善了一点,但仍然不是最好的。照明的另一个重要组成部分是镜面反射(高光)。这取决于反射光和眼睛之间的角度。由于我们不想做所有这些(javascript 很慢),我们将通过对 phong 着色的轻微修改来完成它。我们只是将表面法线乘以一个大于 1 的值。虽然不完美,但效果很好。

表面特性和环境

下一个光是彩色的,球体具有取决于频率的反射特性,并且还有环境光。我们不想为所有这些东西建模,所以我们需要一种方法来伪造它。

这可以通过合成来完成(用于几乎所有 3D 电影制作)。我们一次建立一层照明。 2D API 为我们提供了合成操作,因此我们可以创建多个渐变并将它们分层。

这涉及到更多的数学,但我尽量保持简单。

演示

以下演示对球体进行实时着色(适用于所有径向对称对象)除了画布和鼠标的一些设置代码之外,该演示包含两个部分,主循环通过分层 lights 和函数createGradient 创建渐变。

使用的灯光可以在对象lights 中找到,并且具有控制图层的各种属性。第一层应该使用comp = source-inlum = 1,否则你最终会看到背景显示出来。所有其他图层灯都可以是你想要的。

spec 标志告诉着色器光是镜面反射的,并且必须包含 specPower &gt; 1,因为我不审查它的存在。

灯光的颜色在数组 col 中,分别代表红色、绿色和蓝色。该值可以大于 256 并且小于 0,因为自然界中的光具有巨大的动态范围,并且某些效果需要您将入射光提高到 RGB 像素的 255 限制以上。

我在分层结果中添加了最终的“乘法”。这就是雾里看花法中的魔力。

如果您喜欢代码,请使用值和层。移动鼠标改变光源位置。

这不是真正的照明,它是假的,但只要看起来不错,谁在乎。哈哈

更新

发现了一个错误,因此修复了它,当我在这里时,更改了代码以在您单击鼠标左键时随机化灯光。这样您就可以看到将ctx.globalCompositeOperation 与渐变结合使用时可以实现的照明范围。

var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = (function(){
    var canvas = document.getElementById("canv");
    if(canvas !== null){
        document.body.removeChild(canvas);
    }
    // creates a blank image with 2d context
    canvas = document.createElement("canvas"); 
    canvas.id = "canv";    
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight; 
    canvas.style.position = "absolute";
    canvas.style.top = "0px";
    canvas.style.left = "0px";
    canvas.style.zIndex = 1000;
    canvas.ctx = canvas.getContext("2d"); 
    document.body.appendChild(canvas);
    return canvas;
})();
var ctx = canvas.ctx;

/** fullScreenCanvas.js end **/


/** MouseFull.js begin **/
if(typeof mouse !== "undefined"){  // if the mouse exists 
    if( mouse.removeMouse !== undefined){
        mouse.removeMouse(); // remove prviouse events
    }
}else{
    var mouse;
}
var canvasMouseCallBack = undefined;  // if needed
mouse = (function(){
    var mouse = {
        x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
        interfaceId : 0, buttonLastRaw : 0,  buttonRaw : 0,
        over : false,  // mouse is over the element
        bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
        getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
        startMouse:undefined,
        mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
    };
    function mouseMove(e) {
        var t = e.type, m = mouse;
        m.x = e.offsetX; m.y = e.offsetY;
        if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
        m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
        if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
        } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
        } else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
        } else if (t === "mouseover") { m.over = true;
        } else if (t === "mousewheel") { m.w = e.wheelDelta;
        } else if (t === "DOMMouseScroll") { m.w = -e.detail;}
        if (canvasMouseCallBack) { canvasMouseCallBack(mouse); }
        e.preventDefault();
    }
    function startMouse(element){
        if(element === undefined){
            element = document;
        }
        mouse.element = element;
        mouse.mouseEvents.forEach(
            function(n){
                element.addEventListener(n, mouseMove);
            }
        );
        element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
    }
    mouse.removeMouse = function(){
        if(mouse.element !== undefined){
            mouse.mouseEvents.forEach(
                function(n){
                    mouse.element.removeEventListener(n, mouseMove);
                }
            );
            canvasMouseCallBack = undefined;
        }
    }
    mouse.mouseStart = startMouse;
    return mouse;
})();
if(typeof canvas !== "undefined"){
    mouse.mouseStart(canvas);
}else{
    mouse.mouseStart();
}
/** MouseFull.js end **/

// draws the circle
function drawCircle(c){
    ctx.beginPath();
    ctx.arc(c.x,c.y,c.r,0,Math.PI*2);
    ctx.fill();
}
function drawCircle1(c){
    ctx.beginPath();
    var x  = c.x;
    var y  = c.y;
    var r = c.r * 0.95;
    ctx.moveTo(x,y - r);
    ctx.quadraticCurveTo(x + r * 0.8, y - r         , x + r *1, y - r / 10);
    ctx.quadraticCurveTo(x + r      , y + r/3       , x     , y + r/3);
    ctx.quadraticCurveTo(x - r      , y + r/3       , x - r , y - r /10  );
    ctx.quadraticCurveTo(x - r * 0.8, y - r         , x     , y- r );
    ctx.fill();
}
function drawShadowShadow(circle,light){
    var x = light.x; // get the light position as we will modify it
    var y = light.y;
    var r = circle.r * 1.1;
    var vX = x - circle.x; // get the vector to the light source
    var vY = y - circle.y;
    var dist = -Math.sqrt(vX*vX+vY*vY)*0.3;
    var dir = Math.atan2(vY,vX);
    lx = Math.cos(dir) * dist + circle.x;   // light canb not go past radius
    ly = Math.sin(dir) * dist + circle.y;
    var grd = ctx.createRadialGradient(lx,ly,r * 1/4 ,lx,ly,r);
    grd.addColorStop(0,"rgba(0,0,0,1)");
    grd.addColorStop(1,"rgba(0,0,0,0)");
    ctx.fillStyle = grd;
    drawCircle({x:lx,y:ly,r:r})
}

// 2D light simulation. This is just an approximation and does not match real world stuff
// based on Phong shading.
// x,y,r descript the imagined sphere
// light is the light source 
// ambient is the ambient lighting
// amount is the amount of this layers effect has on the finnal result
function createGradient(circle,light,ambient,amount){
    var r,g,b;  // colour channels
    var x = circle.x; // get lazy coder values
    var y = circle.y;
    var r = circle.r;
    var lx = light.x; // get the light position as we will modify it
    var ly = light.y;
    var vX = light.x - x; // get the vector to the light source
    var vY = light.y - y;
    // get the distance to the light source
    var dist = Math.sqrt(vX*vX+vY*vY);
    // id the light is a specular source then move it to half its position away
    dist *= light.spec ? 0.5 : 1;   
    // get the direction of the light source.
    var dir = Math.atan2(vY,vX);
    
    // fix light position     
    lx = Math.cos(dir)*dist+x;   // light canb not go past radius
    ly = Math.sin(dir)*dist+y;
    // add some dimming so that the light does not wash out.
    dim = 1 - Math.min(1,(dist / (r*4)));
    // add a bit of pretend rotation on the z axis. This will bring in a little backlighting
    var lightRotate = (1-dim) * (Math.PI/2); 
    // spread the light a bit when near the edges. Reduce a bit for spec light
    var spread = Math.sin(lightRotate) * r * (light.spec ? 0.5 : 1);
    
    // create a gradient 
    var grd = ctx.createRadialGradient(lx,ly,spread,x,y,r + dist);
    // use the radius to workout what step will cover a pixel (approx)
    var step = (Math.PI/2)/r;
    // for each pixel going out on the radius add the caclualte light value
    for(var i = 0; i < (Math.PI/2); i += step){
        if(light.spec){
            // fake spec light reduces dim fall off
            // light reflected has sharper falloff
            // do not include back light via Math.abs
            r = Math.max(0,light.col[0] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
            g = Math.max(0,light.col[1] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
            b = Math.max(0,light.col[2] * Math.cos((i + lightRotate)*light.specPower) * 1-(dim * (1/3)) );
        }else{
            // light value is the source lum * the cos of the angle to the light
            // Using the abs value of the refelected light to give fake back light.
            // add a bit of rotation with (lightRotate) 
            // dim to stop washing out 
            // then clamp so does not go below zero
            r = Math.max(0,light.col[0] * Math.abs(Math.cos(i + lightRotate)) * dim );
            g = Math.max(0,light.col[1] * Math.abs(Math.cos(i + lightRotate)) * dim );
            b = Math.max(0,light.col[2] * Math.abs(Math.cos(i + lightRotate)) * dim );
        }
        // add ambient light
        if(light.useAmbient){
        r += ambient[0];
        g += ambient[1];
        b += ambient[2];
        }
        

        // add the colour stop with the amount of the effect we want.
        grd.addColorStop(i/(Math.PI/2),"rgba("+Math.floor(r)+","+Math.floor(g)+","+Math.floor(b)+","+amount+")");
    }
    //return the gradient;
    return grd;
}

// define the circles
var circles = [
    {
        x: canvas.width * (1/2),
        y: canvas.height * (1/2),
        r: canvas.width * (1/8),
    }
]
function R(val){
    return val * Math.random();
}
var lights;
function getLights(){
    return {
        ambient : [10,30,50],
        sources : [
            {
                x: 0,    // position of light 
                y: 0,
                col : [R(255),R(255),R(255)], // RGB intensities can be any value 
                lum : 1,             // total lumanance for this light
                comp : "source-over",  // composite opperation
                spec : false,  // if true then use a pretend specular falloff
                draw : drawCircle,
                useAmbient : true,
            },{  // this light is for a little accent and is at 180 degree from the light
                x: 0,
                y: 0,
                col : [R(255),R(255),R(255)],
                lum : R(1),
                comp : "lighter",
                spec : true,  // if true then you MUST inclue spec power
                specPower : R(3.2),
                draw : drawCircle,
                useAmbient : false,
            },{
                x: canvas.width,
                y: canvas.height,
                col : [R(1255),R(1255),R(1255)],
                lum : R(0.5),
                comp : "lighter",
                spec : false,
                draw : drawCircle,
                useAmbient : false,
    
            },{
                x: canvas.width/2,
                y: canvas.height/2 + canvas.width /4,
                col : [R(155),R(155),R(155)],
                lum : R(1),
                comp : "lighter",
                spec : true,  // if true then you MUST inclue spec power
                specPower : 2.32,
                draw : drawCircle,
                useAmbient : false,
            },{
                x: canvas.width/3,
                y: canvas.height/3,
                col : [R(1255),R(1255),R(1255)],
                lum : R(0.2),
                comp : "multiply",
                spec : false,
                draw : drawCircle,
                useAmbient : false,
            },{
                x: canvas.width/2,
                y: -100,
                col : [R(2255),R(2555),R(2255)],
                lum : R(0.3),
                comp : "lighter",
                spec : false,
                draw : drawCircle1,
                useAmbient : false,
            }
        ]
    }
}
lights = getLights();
/** FrameUpdate.js begin **/
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;
var ch = h / 2;
ctx.font = "20px Arial";
ctx.textAlign = "center";
function update(){
    ctx.setTransform(1,0,0,1,0,0);
    ctx.fillStyle = "#A74"
    ctx.fillRect(0,0,w,h);
    ctx.fillStyle = "black";
    ctx.fillText("Left click to change lights", canvas.width / 2, 20)
    // set the moving light source to that of the mouse
    if(mouse.buttonRaw === 1){
        mouse.buttonRaw = 0;
        lights = getLights();
    }
    lights.sources[0].x = mouse.x;
    lights.sources[0].y = mouse.y;
    if(lights.sources.length > 1){
        lights.sources[1].x = mouse.x;
        lights.sources[1].y = mouse.y;
    }
    drawShadowShadow(circles[0],lights.sources[0])
    //do each sphere 
    for(var i = 0; i < circles.length; i ++){
        // for each sphere do the each light
        var cir = circles[i];
        for(var j = 0; j < lights.sources.length; j ++){
            var light = lights.sources[j];
            ctx.fillStyle = createGradient(cir,light,lights.ambient,light.lum);
            ctx.globalCompositeOperation = light.comp;
            light.draw(circles[i]);
        }
    }
    ctx.globalCompositeOperation = "source-over";    
    
    
    if(!STOP && (mouse.buttonRaw & 4)!== 4){
        requestAnimationFrame(update);
    }else{
        if(typeof log === "function" ){
            log("DONE!")
        }
        STOP = false;
        var can = document.getElementById("canv");
        if(can !== null){
            document.body.removeChild(can);
        }        
        
    }
}

if(typeof clearLog === "function" ){
    clearLog();
}
update();
}
var STOP = false;  // flag to tell demo app to stop 
function resizeEvent(){
var waitForStopped = function(){
    if(!STOP){  // wait for stop to return to false
        demo();
        return;
    }
    setTimeout(waitForStopped,200);
}
STOP = true;
setTimeout(waitForStopped,100);
}
window.addEventListener("resize",resizeEvent);
demo();
/** FrameUpdate.js end **/

【讨论】:

    【解决方案2】:

    正如@danday74 所说,您可以使用渐变来增加圆圈的深度。

    您还可以使用阴影来增加圆圈的深度。

    这是一个演示 3d 甜甜圈的概念验证:

    我留给你设计你想要的圈子

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    
    var PI=Math.PI;
    
    drawShadow(150,150,120,50);
    
    
    function drawShadow(cx,cy,r,strokewidth){
      ctx.save();
      ctx.strokeStyle='white';
      ctx.lineWidth=5;
      ctx.shadowColor='black';
      ctx.shadowBlur=15;
      //
      ctx.beginPath();
      ctx.arc(cx,cy,r-5,0,PI*2);
      ctx.clip();
      //
      ctx.beginPath();
      ctx.arc(cx,cy,r,0,PI*2);
      ctx.stroke();
      //
      ctx.beginPath();
      ctx.arc(cx,cy,r-strokewidth,0,PI*2);
      ctx.stroke();
      ctx.shadowColor='rgba(0,0,0,0)';
      //
      ctx.beginPath();
      ctx.arc(cx,cy,r-strokewidth,0,PI*2);
      ctx.fillStyle='white'
      ctx.fill();
      //
      ctx.restore();
    }
    body{ background-color: white; }
    canvas{border:1px solid red; margin:0 auto; }
    &lt;canvas id="canvas" width=300 height=300&gt;&lt;/canvas&gt;

    【讨论】:

    • 是的。谢谢你的回答,很有帮助。
    【解决方案3】:

    你可以调查的各种想法......

    1 使用图像作为圆的纹理

    2 使用渐变填充圆圈,可能是径向渐变

    3 考虑使用图像蒙版,即定义透明度的黑白蒙版(这里可能不是正确的解决方案)

    【讨论】:

    • 这可以使用渐变看起来像一个 3D 球
    • 可能不值得考虑径向和锥形渐变
    猜你喜欢
    • 2013-01-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-14
    相关资源
    最近更新 更多