2016-02-20 153 views
1

我正在與HTML5canvas一起工作。我已經畫出了一個2D圓圈。現在我想用一個顏色給圓圈加陰影,但陰影看起來像一個3D圓圈。用畫布可以做到這一點嗎?謝謝。如何遮擋畫布中的圓圈

+0

是否使用WebGL的? –

+0

沒有不使用WebGL。這可能嗎? – Gamsh

回答

4

As @ danday74說,您可以使用漸變向您的圓圈添加深度。

您還可以使用陰影將深度添加到您的圈子。

下面是說明3D甜甜圈證明的概念:

enter image description here

我讓你來設計你的期望的繞圈

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; }
<canvas id="canvas" width=300 height=300></canvas>

+1

是的。謝謝你的回答。這非常有幫助。 – Gamsh

2

各種想法,你可以查...

1中使用的圖像的紋理圓

2使用漸變填充圈,可能是一個徑向漸變

3考慮使用圖像蒙版,定義透明度的黑色/白色蒙版(概率不是這裏的正確解決方案)

+0

這是可能的使用一個漸變看起來3D球 – Gamsh

+0

可能不是,但值得考慮的徑向和Conal漸變 – danday74

+1

謝謝你的回答。 – Gamsh

4

假煙霧和鏡子

假球上的燈光。我猜這是一個球體,你說的圓圈,你可能是一個甜甜圈。這項技術也適用於甜甜圈。

所以照明。

Phong光照

最基本的照明模型是蓬(從存儲器)。它使用入射光線和表面法線之間的角度(從90度表面出來的一條線)。反射光量是該角度時間光強度的餘弦。

球體一個容易

由於球是對稱的,這允許我們使用徑向漸變爲在球體上的每個像素,並用於與所述光的球體應用該值直接開銷這產生一個完美的蓬用很少的努力就可以遮蔽球體。

這樣做的代碼。 x,y是球體的中心,r是半徑。當您從球體中心移出時,光線與表面法線之間的角度很容易計算。它從零開始,以Math.PI/2(90度)結束。所以反射值就是該角度的餘弦。

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

該代碼創建一個適合圓的漸變。

國防部荷馬食品

要爲您需要修改我甜甜圈做。甜甜圈具有內,外半徑(R1,R2),所以內部的for循環修改我

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 > 1,因爲我沒有審查它的存在。

燈的顏色在陣列中,代表紅色,綠色和藍色。值可以大於256且小於0,因爲自然界中的光具有巨大的動態範圍,並且某些效果需要將入射光升高到RGB像素的255限制以上。

我給分層結果添加了最後的「乘法」。這是煙霧和鏡像方法中的魔法。

如果你喜歡代碼玩的價值和層次。移動鼠標更改光源位置。

這不是真正的照明它是假的,但只要它看起來好,誰在乎。笑

UPDATE

發現一個錯誤,以便固定它,而我在這裏,改變了代碼,以隨機的燈光,當你點擊鼠標左鍵。這樣您就可以看到將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 **/

+0

有用的光線投射信息! :-) – markE