Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
上節
繪制文本:
可以在Canvas畫布中進行文本的繪制,同時也可以指定繪制文本的字體、大小、對齊方式等,還可以進行文字的紋理填充等;
繪制文本涉及兩個方法,分別為:
fillText(text,x,y,[maxwidth])方法:用填充方式繪制字符串;
strokeText(text,x,y,[maxwidth])方法:用輪廓方式繪制字符串;
這兩個方法都接收4個參數:要繪制的文本字符串、x和y坐標、以及一個可選的maxwidth參數,表示顯示文字時最大的寬度,可以防止文字溢出;
fillText()方法使用fillStyle屬性繪制文本,而strokeText()以strokeStyle屬性為文本描邊;如:
context.fillText('零點程序員', 0, 50);
context.strokeText('零點程序員', 0, 100);
context.fillText('零點程序員', 0, 150, 30);
context.fillStyle = "#00f";
context.fillText('零點程序員', 0, 200);
context.strokeStyle = "#f00";
context.strokeText('零點程序員', 0, 250);
context.strokeText('零點程序員', 0, 250, 30);
示例:繪制包含數據說明的柱狀圖
context.fillStyle = "white";
context.fillRect(0,0,canvas.width,canvas.height);
var data = [100, 50, 20, 30, 100];
var colors = [ "red","orange", "yellow","green", "blue"];
for(var i=0; i<data.length; i++){
var dt = data[i];
context.fillStyle = colors[i];
context.fillRect(25+i*50, 280-dt*2, 50, dt*2);
}
context.fillStyle = "black";
context.lineWidth = 2;
context.beginPath();
context.moveTo(25,10);
context.lineTo(25,280);
context.lineTo(290,280);
context.stroke();
for(var i=0; i<6; i++){
context.fillText((5-i)*20 + "", 4, i*40+80);
context.beginPath();
context.moveTo(25, i*40+80);
context.lineTo(30, i*40+80);
context.stroke();
}
var labels = ["JAN","FEB","MAR","APR","MAY"];
for(var i=0; i<5; i++)
context.fillText(labels[i], 40+ i*50, 300);
在進行文字繪制之前,可以先對該對象的有關文字繪制的屬性進行設置,如:
這三個屬性都有默認值,因此也不是必須顯式去設置它們;
// 使用字體樣式
context.font = 'italic bold 30px sans-serif';
context.textAlign = "end";
context.textBaseline = 'middle';
context.fillText('零點網絡', 150 , 300);
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
再議textAlign屬性,當值為start時,則x坐標表示的是文本左端的位置,如果設置為end,則x坐標表示的是文本右端的位置,如果設置為center,則x坐標表示的是文本的中間的位置,如:
// 默認為start
context.fillText('零點程序員', 100 , 200);
context.textAlign = "start"; // 起點對齊
context.fillText('零點程序員', 100 , 220);
context.textAlign = "center"; // 中間對齊
context.fillText('零點程序員', 100 , 240);
context.textAlign = "end"; // 終點對齊
context.fillText('零點程序員', 100 , 260);
獲取文字寬度:measureText(text)該方法使用要繪制的文本作為參數,返回一個TextMetrics對象,該對象有個最重要的width屬性,表示使用當前指定的字體后,text參數中指定的文字的總文字寬度,如:
var txt = "零點程序員";
var tm = context.measureText(txt);
console.log(tm); // TextMetrics
console.log(tm.width); // 50
measureText()方法利用font、textAlign和textBaseline的當前值計算指定文本的大小,如:
var txt = "零點程序員";
context.fillText(txt, 10, 50);
var tm = context.measureText(txt);
console.log(tm); // TextMetrics
console.log(tm.width); // 50
context.fillStyle = "#00f";
context.font = "italic 20px san-serif";
var tm = context.measureText(txt);
console.log(tm.width); // 100
context.fillText(txt, 10, 100);
context.fillText(tm.width , tm.width + 10, 100);
context.font = "bold 32px san-serif";
var tm2 = context.measureText(txt);
console.log(tm2.width); // 160
context.fillStyle = "purple";
context.fillText(txt, 10, 150);
context.fillText(tm2.width, tm2.width + 10, 150);
如:使用適當的大小繪制文本
var txt = "零點程序員";
var fontSize = 100;
context.font = fontSize + "px Arial";
while(context.measureText(txt).width > 140){
fontSize--;
context.font = fontSize + "px Arial";
}
context.fillText(txt, 50, 50);
context.fillText("字體大小是:" + fontSize + "px", 50, 100);
direction屬性:用來在繪制文本時,描述當前文本方向的屬性,可能的值:
context.font = '48px serif';
context.fillText('zero!', 200, 50);
// context.direction = 'rtl';
context.fillText('zero!', 200, 130);
filter屬性:濾鏡,提供模糊、灰度等過濾效果的屬性,類似于CSS filter屬性,并且接受相同的函數;
context.filter = "blur(5px)";
context.font = "48px serif";
context.strokeText("大師哥王唯", 100, 100);
Chrome還定義了多個有關字體的屬性:
示例:繪制帶有文字的餅形圖
function PieChart (context){
this.context = context || document.getElementById("canvas").getContext("2d");
this.x = this.context.canvas.width/2 - 30;
this.y = this.context.canvas.height/2;
this.r = 120;
this.outLine = 20;
this.dataList = null;
}
PieChart.prototype = {
constructor:PieChart,
init:function(dataList){
this.dataList = dataList || [{title:"默認",value:100}];
this.transformAngle();
this.drawPie();
},
drawPie:function(){
var startAngle = 0,endAngle;
for(var i = 0 ; i < this.dataList.length ; i++){
var item = this.dataList[i];
endAngle = startAngle + item.angle;
this.context.beginPath();
this.context.moveTo(this.x,this.y);
this.context.arc(this.x,this.y,this.r,startAngle,endAngle,false);
var color= this.context.strokeStyle= this.context.fillStyle= this.getRandomColor();
this.context.stroke();
this.context.fill();
this.drawPieTitle(startAngle,item.angle,color,item.title)
this.drawPieLegend(i,item.title);
startAngle = endAngle;
}
},
drawPieTitle:function(startAngle,angle,color,title){
var edge = this.r + this.outLine;
var edgeX = Math.cos(startAngle + angle / 2) * edge;
var edgeY = Math.sin(startAngle + angle / 2) * edge;
var outX = this.x + edgeX;
var outY = this.y + edgeY;
this.context.beginPath();
this.context.moveTo(this.x,this.y);
this.context.lineTo(outX,outY);
this.context.strokeStyle = color;
this.context.stroke();
var textWidth = this.context.measureText(title).width + 5;
var lineX = outX > this.x ? outX + textWidth : outX - textWidth;
this.context.lineTo(lineX,outY);
this.context.stroke();
this.context.font = "15px KaiTi";
this.context.textAlign = outX > this.x ? "left" : "right";
this.context.textBaseline = "bottom";
this.context.fillText(title,outX,outY);
},
drawPieLegend:function(index,title){
var space = 10;
var rectW = 40;
var rectH = 20;
var rectX = this.x + this.r + 80;
var rectY = this.y + (index * 30);
this.context.fillRect(rectX,rectY,rectW,rectH);
// this.context.beginPath();
this.context.textAlign = 'left';
this.context.textBaseline = 'top';
this.context.fillStyle = "#000";
this.context.fillText(title,rectX + rectW + space,rectY);
},
getRandomColor:function(){
var r = Math.floor(Math.random() * 256);
var g = Math.floor(Math.random() * 256);
var b = Math.floor(Math.random() * 256);
return 'rgb('+r+','+g+','+b+')';
},
transformAngle:function(){
var self = this;
var total = 0;
this.dataList.forEach(function(item,i){
total += item.value;
})
this.dataList.forEach(function(item,i){
self.dataList[i].angle = 2 * Math.PI * item.value/total;
})
},
}
var data = [{value:20,title:"UI"},{value:26,title:"java"},
{value:20,title:"iOS"},{value:63,title:"H5"},{value:25,title:"Node"}]
var pie = new PieChart().init(data);
裁切路徑:
在繪制圖形的時候,如果只保留圖形的一部分,可以使用裁切路徑;
使用clip()方法,可以將當前創建的路徑設置為當前剪切路徑;
使用原理是:首先在畫布內使用路徑,只繪制該路徑所包括區域內的圖像;再使用clip()方法,該方法創建一個裁切路徑,使用該路徑對canvas畫布設置一個裁剪區域;如:
context.arc(100, 100, 75, 0, Math.PI*2, false);
context.clip();
context.fillRect(0, 0, 100,100);
默認情況下,canvas 有一個與它自身一樣大的裁切路徑;
示例:
context.font = "bold 60pt sans-serif";
context.lineWidth = 2;
context.strokeStyle = "#F00";
context.strokeText("零點程序員", 15, 330);
context.strokeRect(175,25,50,350);
context.beginPath();
context.moveTo(200, 50);
context.lineTo(350, 350);
context.lineTo(50, 350);
context.closePath();
context.clip();
context.lineWidth = 10;
context.stroke();
context.fillStyle = "#aaa";
context.fillRect(175,25,50,350);
context.fillStyle = "#888";
context.fillText("零點程序員", 15, 330);
示例:繪制一個五角形裁切路徑
var image = new Image();
image.src = "images/1.png";
image.onload = function(){
createStar(context);
context.drawImage(image,-50,-150,300,300);
}
function createStar(context){
var dx = 100, dy = 0, s = 150, dig = Math.PI / 5 * 4;
context.beginPath();
context.translate(100, 150);
for(var i=0; i<5; i++){
var x = Math.sin(i*dig);
var y = Math.cos(i*dig);
context.lineTo(dx+x*s, dy+y*s);
}
context.clip();
}
裁剪區域一旦設置好后,后續繪制的所有圖形都使用這個裁切區域;如果要取消這個已經設置好的裁剪區域,由于沒有重置裁切路徑的方法,所以,需要使用繪制狀態的保存與恢復功能;即通過save()和restore(),對之后繪制的圖像取消裁剪區域;
示例:探照燈效果
<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;"></canvas>
<script>
var rot=10;
var canvas=document.getElementById('myCanvas');
var context=canvas.getContext('2d');
setInterval("draw()",100);
function draw(){
context.clearRect(0,0,400,400);
context.save();
context.fillStyle="black";
context.fillRect(0,0,400,400);
context.beginPath();
context.arc(rot, 200, 40, 0, Math.PI*2, true);
context.closePath();
context.fillStyle="white";
context.fill();
context.clip();
context.font="bold 45px 隸書";
context.textAlign="center";
context.textBaseline="middle";
context.fillStyle="#FF0000";
context.fillText("零點程序員大師哥",200,200);
context.restore();
rot=rot+10;
if (rot>400) rot=10;
}
</script>
示例:隨機星星
context.fillRect(0,0,150,150);
context.translate(75,75);
context.beginPath();
context.arc(0,0,60,0,Math.PI*2,true);
context.clip();
var lingrad = context.createLinearGradient(0,-75,0,75);
lingrad.addColorStop(0, '#232256');
lingrad.addColorStop(1, '#143778');
context.fillStyle = lingrad;
context.fillRect(-75,-75,150,150);
for (var j=1; j<50; j++){
context.save();
context.fillStyle = '#fff';
context.translate(75-Math.floor(Math.random()*150),
75-Math.floor(Math.random()*150));
drawStar(context, Math.floor(Math.random()*4)+2);
context.restore();
}
function drawStar(context, r){
context.save();
context.beginPath()
context.moveTo(r, 0);
for (var i=0; i<9; i++){
context.rotate(Math.PI/5); // 36度
if(i%2 == 0) {
context.lineTo((r/0.525731)*0.200811, 0);
} else {
context.lineTo(r, 0);
}
}
context.closePath();
context.fill();
context.restore();
}
繪制圖像:
2D上下文內置了對圖像(位圖)的支持,可以讀取本地以及網絡中的圖片,再將該圖像的像素內容繪制(復制)在畫布中;必要的時候,還可以對圖片進行縮放和旋轉;
使用drawImage()方法即可繪制圖像;
drawImage(image,x,y):繪制圖像,參數image為一個將要被繪制到畫布上的源圖片,x和y指定了待繪制圖片的左上角的坐標;繪制的圖像和原圖大小相同;
繪制圖像時,首先需要一個Image對象或一個<img>元素,如:
var image = document.images[0];
context.drawImage(image, 50, 50);
如果使用Image(),設定好image的src屬性后,并不一定立刻就能把圖像繪制完畢,如有時該圖像來自網絡的比較大的圖像文件,就需要完全下載后才能繪制,所以需要在image的onload事件進行處理,此時就可以一邊裝載一邊繪制了,如:
image = new Image();
image.src="images/1.jpg";
image.onload = function(){
drawImg(context,image);
}
function drawImg(context,image){
for(var i=0;i<7;i++){
context.drawImage(image,i*50,i*25,100,100);
}
}
示例:一個簡單的線圖
var img = new Image();
img.onload = function(){
context.drawImage(img,0,0);
context.beginPath();
context.moveTo(30,96);
context.lineTo(70,66);
context.lineTo(103,76);
context.lineTo(170,15);
context.stroke();
}
img.src = 'images/backdrop.png';
drawImage(image, x, y, w, h):使用w和h設置繪制的圖像的大小,可以用來圖像縮放;會繪制整個源圖像;如:
context.drawImage(image, 50, 50, 200, 160);
按比例指定大小,如:
var image = document.images[0];
var w = image.width,
h = image.height;
var ratio = w / h;
var nw = 200,
nh = nw / ratio;
context.drawImage(image, 50, 50, nw, nh);
示例:平鋪圖像:
var img = new Image();
img.onload = function(){
for (var i=0; i<4; i++){
for (var j=0; j<3; j++){
context.drawImage(img, j*50, i*38, 50, 38);
}
}
};
img.src = 'images/1.jpg';
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)方法:可以將畫布中已繪制好的圖像的全部或者局部區域復制到畫布中的另一個位置上;sx和sy表示源圖像的被復制區域的坐標;sw和sh表示源圖像的被復制區域的寬和高;dx和dy表示復制后的目標圖像的坐標;dw和dh表示復制后的目標圖像的寬和高;
該方法可以只復制圖像的局部,只要將sx和sy設為局部區域的起始坐標,將sw與sh設為局部區域的寬和高即可;該方法也可以用來縮放源圖像,只要將dw和dh設為縮放后的寬和高即可;
如:局部放大復制到另外一個位置:
context.drawImage(image, 100, 100, 150, 100, 10, 10, 100, 50);
示例:相框:
context.drawImage(document.getElementById('source'),
33,71,104,124,21,20,87,104); // 圖片
context.drawImage(document.getElementById('frame'),0,0);
示例:畫廊
window.onload = function(){
document.body.style.backgroundColor = "#4f191A";
var canvas, context;
var frameImg = new Image();
frameImg.src = "images/picture_frame.png";
frameImg.onload = function(){
var imgsArr = ["images/1.jpg","images/2.jpg","images/3.jpg","images/4.jpg","images/5.jpg","images/6.jpg","images/7.jpg","images/8.jpg",]
for (i=0; i<imgsArr.length; i++){
(function(i){
var img = new Image();
img.src = imgsArr[i];
img.onload = function(){
canvas = document.createElement('canvas');
canvas.setAttribute('width',132);
canvas.setAttribute('height',150);
document.body.appendChild(canvas);
context = canvas.getContext('2d');
context.drawImage(img,15,20,102,110);
context.drawImage(frameImg,0,0);
}
})(i);
}
}
}
除了使用image作為源圖像之外,也可以傳入一個<canvas>元素,這樣,就可以把另一個畫布內容繪制到當前畫布上;除此之外,還可以是一個Video元素、ImageBitmap對象或使用dataURL嵌入的圖像;
如,使用視頻幀:
<canvas id="canvas" width="600" height="400"></canvas>
<video width="600" height="400" src="media/video.mp4" controls></video>
<script>
var video = document.querySelector("video");
var canvas = document.getElementById("canvas");
var interId;
var context = canvas.getContext("2d");
video.onplay = function(){
interId = setInterval(function(){
context.clearRect(0,0,600,400);
context.fillRect(0,0,600,400);
context.drawImage(video, 0, 70, 600, 440);
context.font = "20px 微軟雅黑";
context.strokeStyle = "#999";
context.strokeText("零點程序員", 50, 50);
},16);
}
video.onpause = function(){
clearInterval(interId);
}
</script>
圖像平鋪:
所謂的平鋪就是按一定比例縮小后的圖像填滿畫布,有兩種方法,一是使用drawImage方法:
var image = new Image();
image.src = "images/1.jpg";
image.onload = function(){
drawImage(canvas,context,image);
}
function drawImage(canvas,context,image){
var scale = 20;
var w = image.width / scale; var h = image.height / scale;
var numX = canvas.width / w; var numY = canvas.height / h;
for(var i=0;i<numX;i++){
for(var j=0;j<numY;j++){
context.drawImage(image,i*w,j*h,w,h);
}
}
}
第二種方法:可以使用context的createPattern(image, type)方法,參數type指定了重復的類型,其可能的值:no-repeat、repeat-x、repeat-y、repeat;創建完createPattern對象后,再賦給fillStyle即可;
var image = new Image();
image.src = "images/1.jpg";
image.onload = function(){
var pattern = context.createPattern(image,"repeat");
context.fillStyle = pattern;
context.fillRect(0,0,400,300);
}
圖形、圖像的混合與組合(composite):
混合(合成)圖像:
所謂圖像混合(合成),是指使用某種數學公式將兩幅圖像混合在一起;從這個角度說,圖像混合有些類似于對一幅圖像使用一定的透明度后將其放置在另一幅圖像上,但是事實上圖像混合技術能夠實現比透明度更好的混合效果;
在混合圖像時,將疊放在一起的兩幅圖像進行逐像素顏色比較,放置于底層的圖像像素顏色稱為基色,放置于上層的圖像像素顏色稱為混合色,將這兩種像素顏色按一定的計算公式計算后得到的像素顏色稱為結果色,最后對合成后的圖像像素應用結果色;
為了使用混合技術,需要使用context的屬性:globalCompositeOperation;
globalCompositeOperation屬性:
表示后繪制的圖形怎樣與先繪制的圖形結合,其可能的值如下:
context.fillStyle = "#ff0000";
context.fillRect(50,50,100,100);
context.globalCompositeOperation = "screen";
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(50,100,100,100);
context.globalCompositeOperation = "darken";
var image = new Image();
image.src = "images/s.png";
image.onload = function(){
context.drawImage(image,0,0);
var image2 = new Image();
image2.src = "images/2.png";
image2.onload = function(){
context.drawImage(image2,0,0);
}
}
有的時候,并不希望進行合成;比如:已經使用半透明像素在畫布中繪制了內容,這個時候,想要進行臨時切換,然后再恢復到原先的狀態;這個時候最簡單的方法就是:將使用drawImage()方法將畫布內容(或一部分內容)復制到一張屏幕外畫布中;但是,保存的像素都是半透明的,這個時候合成是開啟的,它們并不會完全抹除臨時繪制的內容;因此,在這種情況下,就需要一種方式將合成關閉:不論像素是否透明,都會繪制源像素并忽略目標像素;
組合圖像:
在繪制多個圖形的時候,會出現重疊的現象;此時可以將多塊圖形進行組合,也是使用globalCompositeOperation屬性去指定組合方式,其可能的值如下:
context.fillStyle = "#F00";
context.fillRect(50,50,200,200);
// context.globalCompositeOperation = "source-over";
// context.globalCompositeOperation = "source-atop";
// context.globalCompositeOperation = "source-in";
// context.globalCompositeOperation = "source-out";
// context.globalCompositeOperation = "destination-over";
// context.globalCompositeOperation = "destination-atop";
// context.globalCompositeOperation = "destination-out";
// context.globalCompositeOperation = "copy";
// context.globalCompositeOperation = "lighter";
context.globalCompositeOperation = "xor";
// context.fillStyle = "#0F0"; // 硬透明度
var g = context.createRadialGradient(200,200,20, 200,200,120);// 軟透明度
g.addColorStop(0.0, "#0F0");
g.addColorStop(1.0, "#00F");
context.fillStyle = g;
context.arc(200,200,120,0,Math.PI*2);
context.fill();
觀察不同值的狀態:
<input id="changeBtn" type="button" value="下一個" >
<canvas id="canvas" width="1000" height="600"></canvas>
<script>
var canvas = document.getElementsByTagName("canvas")[0];
var arr = new Array("source-over","source-in","source-out","source-atop","destination-over","destination-in","destination-out","destination-atop","lighter","xor","copy");
var i = 0;
drawComposite(i);
var changeBtn = document.getElementById("changeBtn");
changeBtn.addEventListener("click", function(){
if(i++ == arr.length - 1)
i = 0;
drawComposite(i);
})
function drawComposite(i){
var context = canvas.getContext("2d");
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "blue";
context.fillRect(10,10,100,100);
context.globalCompositeOperation = arr[i];
context.beginPath();
context.fillStyle = "red";
context.arc(100,100,60,0,Math.PI*2);
context.closePath();
context.fill();
context.restore();
context.font = "24px 微軟雅黑";
context.fillText(i + ": " + arr[i], 0, 200);
}
</script>
組合合成示例:
<script>
// 定義了一些全局變量
var canvas1 = document.createElement("canvas");
var canvas2 = document.createElement("canvas");
var gco = [ 'source-over','source-in','source-out','source-atop',
'destination-over','destination-in','destination-out','destination-atop',
'lighter', 'copy','xor',
'multiply', 'screen', 'overlay', 'darken',
'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'
].reverse();
var gcoText = [
'這是默認設置,并在現有畫布上下文之上繪制新圖形。',
'新圖形只在新圖形和目標畫布重疊的地方繪制。其他的都是透明的。',
'在不與現有畫布內容重疊的地方繪制新圖形。',
'新圖形只在與現有畫布內容重疊的地方繪制。',
'在現有的畫布內容后面繪制新的圖形。',
'現有的畫布內容保持在新圖形和現有畫布內容重疊的位置。其他的都是透明的。',
'現有內容保持在新圖形不重疊的地方。',
'現有的畫布只保留與新圖形重疊的部分,新的圖形是在畫布內容后面繪制的。',
'兩個重疊圖形的顏色是通過顏色值相加來確定的。',
'只顯示新圖形。',
'圖像中,那些重疊和正常繪制之外的其他地方是透明的。',
'將頂層像素與底層相應像素相乘,結果是一幅更黑暗的圖片。',
'像素被倒轉,相乘,再倒轉,結果是一幅更明亮的圖片。',
'multiply 和 screen 的結合,原本暗的地方更暗,原本亮的地方更亮。',
'保留兩個圖層中最暗的像素。',
'保留兩個圖層中最亮的像素。',
'將底層除以頂層的反置。',
'將反置的底層除以頂層,然后將結果反過來。',
'屏幕相乘(A combination of multiply and screen)類似于疊加,但上下圖層互換了。',
'用頂層減去底層或者相反來得到一個正值。',
'一個柔和版本的強光(hard-light)。純黑或純白不會導致純黑或純白。',
'和 difference 相似,但對比度較低。',
'保留了底層的亮度(luma)和色度(chroma),同時采用了頂層的色調(hue)。',
'保留底層的亮度(luma)和色調(hue),同時采用頂層的色度(chroma)。',
'保留了底層的亮度(luma),同時采用了頂層的色調 (hue) 和色度 (chroma)。',
'保持底層的色調(hue)和色度(chroma),同時采用頂層的亮度(luma)。'
].reverse();
var width = 320;
var height = 340;
window.onload = function() {
var lum = {
r: 0.33,
g: 0.33,
b: 0.33
};
canvas1.width = width;
canvas1.height = height;
canvas2.width = width;
canvas2.height = height;
colorSphere();
lightMix()
runComposite();
return;
};
var colorSphere = function(element) {
var ctx = canvas1.getContext("2d");
var width = 360;
var halfWidth = width / 2;
var rotate = (1 / 360) * Math.PI * 2; // per degree
var offset = 0; // scrollbar offset
var oleft = -20;
var otop = -20;
for (var n = 0; n <= 359; n ++) {
var gradient = ctx.createLinearGradient(oleft + halfWidth, otop, oleft + halfWidth, otop + halfWidth);
var color = Color.HSV_RGB({ H: (n + 300) % 360, S: 100, V: 100 });
gradient.addColorStop(0, "rgba(0,0,0,0)");
gradient.addColorStop(0.7, "rgba("+color.R+","+color.G+","+color.B+",1)");
gradient.addColorStop(1, "rgba(255,255,255,1)");
ctx.beginPath();
ctx.moveTo(oleft + halfWidth, otop);
ctx.lineTo(oleft + halfWidth, otop + halfWidth);
ctx.lineTo(oleft + halfWidth + 6, otop);
ctx.fillStyle = gradient;
ctx.fill();
ctx.translate(oleft + halfWidth, otop + halfWidth);
ctx.rotate(rotate);
ctx.translate(-(oleft + halfWidth), -(otop + halfWidth));
}
ctx.beginPath();
ctx.fillStyle = "#00f";
ctx.fillRect(15,15,30,30)
ctx.fill();
return ctx.canvas;
};
var lightMix = function() {
var ctx = canvas2.getContext("2d");
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.beginPath();
ctx.fillStyle = "rgba(255,0,0,1)";
ctx.arc(100, 200, 100, Math.PI*2, 0, false);
ctx.fill()
ctx.beginPath();
ctx.fillStyle = "rgba(0,0,255,1)";
ctx.arc(220, 200, 100, Math.PI*2, 0, false);
ctx.fill()
ctx.beginPath();
ctx.fillStyle = "rgba(0,255,0,1)";
ctx.arc(160, 100, 100, Math.PI*2, 0, false);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.fillStyle = "#f00";
ctx.fillRect(0,0,30,30)
ctx.fill();
};
function createCanvas() {
var canvas = document.createElement("canvas");
canvas.style.background = "url("+op_8x8.data+")";
canvas.style.border = "1px solid #000";
canvas.style.margin = "5px";
canvas.width = width/2;
canvas.height = height/2;
return canvas;
}
function runComposite() {
var dl = document.createElement("dl");
document.body.appendChild(dl);
while(gco.length) {
var pop = gco.pop();
var dt = document.createElement("dt");
dt.textContent = pop;
dl.appendChild(dt);
var dd = document.createElement("dd");
var p = document.createElement("p");
p.textContent = gcoText.pop();
dd.appendChild(p);
var canvasToDrawOn = createCanvas();
var canvasToDrawFrom = createCanvas();
var canvasToDrawResult = createCanvas();
var ctx = canvasToDrawOn.getContext('2d');
ctx.clearRect(0, 0, width, height)
ctx.save();
ctx.drawImage(canvas1, 0, 0, width/2, height/2);
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(0, height/2 - 20, width/2, 20);
ctx.fillStyle = "#FFF";
ctx.font = "14px arial"; // [?ɡ?z?st??] 現存的,存在的
ctx.fillText('existing content', 5, height/2 - 5);
ctx.restore();
var ctx = canvasToDrawFrom.getContext('2d');
ctx.clearRect(0, 0, width, height)
ctx.save();
ctx.drawImage(canvas2, 0, 0, width/2, height/2);
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(0, height/2 - 20, width/2, 20);
ctx.fillStyle = "#FFF";
ctx.font = "14px arial";
ctx.fillText('new content', 5, height/2 - 5);
ctx.restore();
var ctx = canvasToDrawResult.getContext('2d');
ctx.clearRect(0, 0, width, height)
ctx.save();
ctx.drawImage(canvas1, 0, 0, width/2, height/2);
ctx.globalCompositeOperation = pop;
ctx.drawImage(canvas2, 0, 0, width/2, height/2);
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(0, height/2 - 20, width/2, 20);
ctx.fillStyle = "#FFF";
ctx.font = "14px arial";
ctx.fillText(pop, 5, height/2 - 5);
ctx.restore();
dd.appendChild(canvasToDrawOn);
dd.appendChild(canvasToDrawFrom);
dd.appendChild(canvasToDrawResult);
dl.appendChild(dd);
}
};
// HSV (1978) = H: Hue / S: Saturation / V: Value
Color = {};
Color.HSV_RGB = function (o) {
var H = o.H / 360,
S = o.S / 100,
V = o.V / 100,
R, G, B;
var A, B, C, D;
if (S == 0) {
R = G = B = Math.round(V * 255);
} else {
if (H >= 1) H = 0;
H = 6 * H;
D = H - Math.floor(H);
A = Math.round(255 * V * (1 - S));
B = Math.round(255 * V * (1 - (S * D)));
C = Math.round(255 * V * (1 - (S * (1 - D))));
V = Math.round(255 * V);
switch (Math.floor(H)) {
case 0:
R = V;
G = C;
B = A;
break;
case 1:
R = B;
G = V;
B = A;
break;
case 2:
R = A;
G = V;
B = C;
break;
case 3:
R = A;
G = B;
B = V;
break;
case 4:
R = C;
G = A;
B = V;
break;
case 5:
R = V;
G = A;
B = B;
break;
}
}
return {
R: R,
G: G,
B: B
};
};
var createInterlace = function (size, color1, color2) {
var proto = document.createElement("canvas").getContext("2d");
proto.canvas.width = size * 2;
proto.canvas.height = size * 2;
proto.fillStyle = color1; // top-left
proto.fillRect(0, 0, size, size);
proto.fillStyle = color2; // top-right
proto.fillRect(size, 0, size, size);
proto.fillStyle = color2; // bottom-left
proto.fillRect(0, size, size, size);
proto.fillStyle = color1; // bottom-right
proto.fillRect(size, size, size, size);
var pattern = proto.createPattern(proto.canvas, "repeat");
pattern.data = proto.canvas.toDataURL();
return pattern;
};
var op_8x8 = createInterlace(8, "#FFF", "#eee");
</script>
像素處理;
使用canvas API能夠獲取圖像中的每個像素,并且能夠得到該像素的顏色的RGB值或RGBA值;具體就是使用context的getImageData(x, y, width, height)方法來獲取原始圖像數據,參數x、y表示所獲取區域的起始坐標,width、height表示所獲取區域的寬和高;
var image = document.getElementsByTagName("img")[0];
context.drawImage(image,0,0);
var imagedata = context.getImageData(0, 0, image.width, image.height);
console.log(imagedata); // ImageData
該方法返回一個ImageData類型的對象,表示畫布矩形區域中的原始像素信息,通過該對象可以操縱像素數據、直接讀取或將數據數組寫入該對象中,其具有width、height、colorSpace、data等屬性;其中,data屬性是一個保存像素的Uint8ClampedArray類型化數組視圖,內容類似[r1,g1,b1,a1,r2,g2,b2,a2,..],其中每4個元素表示一個像素信息,即為R、G、B和A分量,也就是r1、g1、b1、a1為一個像素的紅、綠、藍與透明度的值;data.length為所取得像素的數量的4倍;
某個像素的索引位置(n為第n個像素)為:(n-1)*4+0(R)、(n-1)*4+1(G)、(n-1)*4+2(B)、(n-1)*4+3(A),分別為第n個像素的 R/G/B/A 分量值;
// 取得第一個像素的值
var red = imagedata.data[0],
green = imagedata.data[1],
blue = imagedata.data[2],
alpha = imagedata.data[3];
var color = [red, green, blue, alpha];
console.log(color);
var red = imagedata.data[4],
green = imagedata.data[5],
blue = imagedata.data[6],
alpha = imagedata.data[7];
var color = [red, green, blue, alpha];
console.log(color); // [57, 133, 254, 255]
var n = 9;
var red = imagedata.data[(n-1)*4+0],
green = imagedata.data[(n-1)*4+1],
blue = imagedata.data[(n-1)*4+2],
alpha = imagedata.data[(n-1)*4+3];
var color = [red, green, blue, alpha];
console.log(color);
按行和列獲取某個像素RGBA分量值:row * imageData.width * 4 + col * 4 + 0|1|2|3或(row * imageData.width + col) * 4 + 0|1|2|3;
// 讀取圖片中位于索引為50行、索引為200列的像素的藍色
var bluePixel = imagedata.data[50 * imagedata.width * 4 + 200 * 4 + 2];
console.log(bluePixel); // 254
// 封裝一個函數
function getPixel(row, col){
var pixels = [];
for(var i=0; i<4; i++){
pixels.push(imagedata.data[(row * imagedata.width + 200) * 4 + i]);
}
return pixels;
}
var pixels = getPixel(50, 200);
console.log(pixels);
任何在畫布以外的元素都會被返回成一個透明黑的 ImageData 對像;
var imagedata = context.getImageData(image.width, 0, image.width, image.height);
console.log(imagedata);
通過類型為Uint8ClampedArray的data屬性,不僅能直接訪問到原始圖像像素數據,還能夠以各種方式來操作這些數據,例如,可以修改圖像像素數據,創建一個簡單的灰階過濾器,如:
var red, green, blue, alpha;
var average;
var data = imagedata.data;
for(var i=0, len = data.length; i<len; i+=4){
red = data[i];
green = data[i+1];
blue = data[i+2];
alpha = data[i+3];
average = Math.floor((red + green + blue) / 3);
data[i] = data[i+1] = data[i+2] = average;
}
imagedata.data = data;
context.putImageData(imagedata, 0, 0);
用getImageData方法獲取圖片信息時,該圖像不能跨域;
img元素中的crossorigin屬性,該屬性是HTML5新增的屬性,以腳本中,也有同名的屬性,其決定了圖片獲取過程中是否開啟CORS功能;有兩個可能值:
默認情況下,如果沒有使用這個屬性,說明沒有開啟CORS,且值為null;
示例:顏色選擇器
<style>
.container{
display: flex;
}
.container div{width:200px; border:1px solid;}
</style>
<div class="container">
<canvas id="drawing" width="300" height="200"></canvas>
<div id="hovered-color"></div>
<div id="selected-color"></div>
</div>
<script>
window.onload = function(){
var img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'images/1.jpg';
var canvas = document.getElementById('drawing');
var context = canvas.getContext('2d');
img.onload = function() {
context.drawImage(img, 0, 0);
img.style.display = 'none';
};
var hoveredColor = document.getElementById('hovered-color');
var selectedColor = document.getElementById('selected-color');
canvas.addEventListener('mousemove', function(event) {
pick(event, hoveredColor);
});
canvas.addEventListener('click', function(event) {
pick(event, selectedColor);
});
function pick(event, destination) {
var x = event.layerX;
var y = event.layerY;
var pixel = context.getImageData(x, y, 1, 1);
var data = pixel.data;
var r = data[0], g = data[1], b = data[2], a = data[3] / 255;
var rgba = "rgba(" + r + ", "+ g +", "+ b +", " + a + ")";
destination.style.background = rgba;
destination.textContent = rgba;
return rgba;
}
}
</script>
取得像素后,就可以對這些像素進行處理,如蒙版處理、面部識別等較復雜的圖像處理操作;
使用putImageData(imagedata, dx, dy[, dirtyX, dirtyY, dirtyWidth, dirtyHeight])方法可以將一個已有的ImageData對象繪制到畫布上,參數dx和dy表示重繪圖像的起點坐標,dirtyX、dirtyY、dirtyWidth、dirtyHeight為可選,它們給出一個矩形的起點坐標及寬高,如果使用這4個參數,則只繪制像素數組中在這個矩形范圍內的圖像;
context.drawImage(image, 0, 0);
var imagedata = context.getImageData(0,0,image.width,image.height);
context.clearRect(0, 0, 800, 600);
context.putImageData(imagedata, 0, 0, 200, 200, 200, 100);
也可以寫入到另外一個canvas中,如:
// ...
var canvas1 = document.getElementById("canvas1");
var ctx = canvas1.getContext("2d");
ctx.putImageData(imagedata, 0, 0, 200, 200, 200, 100);
context.drawImage(image, 0, 0);
var imagedata = context.getImageData(0,0,image.width,image.height);
for(var i=0, len=imagedata.data.length; i<len; i++){
imagedata.data[i+0] = 255 - imagedata.data[i+0]; // red
imagedata.data[i+1] = 255 - imagedata.data[i+2]; // green
imagedata.data[i+2] = 255 - imagedata.data[i+1]; // blue
}
context.putImageData(imagedata, 0, 0);
putImageData()會按照默認的坐標系來處理,不受畫布變換矩陣的影響,而且,會忽略所有的圖形屬性;不會進行任何合成操作,也不會用globalAlpha乘以像素來顯示,更不會繪制陰影;
putImageData()方法的原理:
function putImageData(context, imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
var data = imageData.data;
var width = imageData.width;
var height = imageData.height;
dirtyX = dirtyX || 0;
dirtyY = dirtyY || 0;
dirtyWidth = dirtyWidth !== undefined ? dirtyWidth : width;
dirtyHeight = dirtyHeight !== undefined ? dirtyHeight : height;
var limitBottom = dirtyY + dirtyHeight;
var limitRight = dirtyX + dirtyWidth;
for (var y = dirtyY; y < limitBottom; y++) {
for (var x = dirtyX; x < limitRight; x++) {
var pos = y * width + x;
context.fillStyle = 'rgba(' + data[pos*4+0]
+ ',' + data[pos*4+1]
+ ',' + data[pos*4+2]
+ ',' + (data[pos*4+3]/255) + ')';
context.fillRect(x + dx, y + dy, 1, 1);
}
}
}
context.fillRect(0,0,100,100);
var imagedata = context.getImageData(0,0,100,100);
putImageData(context, imagedata, 150, 0, 50, 50, 25, 25);
示例:圖片灰度和反相顏色
<canvas id="canvas" width="300" height="200"></canvas>
<p>
<input type="radio" id="original" name="color" value="original" checked>
<label for="original">Original</label>
<input type="radio" id="grayscale" name="color" value="grayscale">
<label for="grayscale">Grayscale</label>
<input type="radio" id="inverted" name="color" value="inverted">
<label for="inverted">Inverted</label>
</p>
<script>
var img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'images/2.jpg';
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
img.onload = function() {
context.drawImage(img, 0, 0);
};
var original = function() {
context.drawImage(img, 0, 0);
};
var grayscale = function() {
context.drawImage(img, 0, 0);
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // red
data[i + 1] = avg; // green
data[i + 2] = avg; // blue
}
context.putImageData(imageData, 0, 0);
};
var invert = function() {
context.drawImage(img, 0, 0);
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // red
data[i + 1] = 255 - data[i + 1]; // green
data[i + 2] = 255 - data[i + 2]; // blue
}
context.putImageData(imageData, 0, 0);
};
var inputs = document.querySelectorAll('[name=color]');
for (var input of inputs) {
input.addEventListener("change", function(evt) {
switch (evt.target.value) {
case "inverted":
return invert();
case "grayscale":
return grayscale();
default:
return original();
}
});
}
</script>
createImageData(width, height | imagedata)方法:
可以創建一個空的ImageData對象,該對象中的像素是可寫的,因此,可以對它們進行設置;
參數width和height為新對象的寬和高;imagedata為一個已有的ImageData對象,即復制一個和它具有相同的高和寬的對象,而圖像自身不被復制;
context.rect(10, 10, 100, 100);
context.fill();
var imgdata = context.createImageData(100, 100);
console.log(imgdata); // ImageData
console.log(context.createImageData(imgdata)); // ImageData
默認情況下,這個空的ImageData對象的像素全部被預設為透明黑;如果width和height指定為負值,會被處理成相應的正值:
有了ImageData對象后,再通過putImageData()方法將這些像素復制回畫布中;如:
var imagedata = context.getImageData(0, 0, canvas.width, canvas.height);
var data = context.createImageData(imagedata);
console.log(imagedata);
for(var i=0; i<imagedata.data.length; i++){
data.data[i] = imagedata.data[i];
}
console.log(data);
var canvas1 = document.getElementById("canvas1");
var ctx = canvas1.getContext("2d");
ctx.putImageData(data, 0, 0);
示例:在一個畫布中的圖形要創建一種簡單的動態模糊或“涂抺”效果;
function smear(c, n, x, y, w, h){
var pixels = c.getImageData(x,y,w,h);
var width = pixels.width, height = pixels.height;
var data = pixels.data;
var m = n - 1;
for(var row=0; row<height; row++){
var i = row * width * 4 + 4;
for(var col=1; col<width; col++, i+=4){
data[i] = (data[i] + data[i-4]*m) / n;
data[i+1] = (data[i+1] + data[i-3]*m) / n;
data[i+2] = (data[i+2] + data[i-2]*m) / n;
data[i+3] = (data[i]+3 + data[i-1]*m) / n;
}
}
c.putImageData(pixels, x, y);
}
var image = new Image();
image.src = "images/1.jpg";
image.onload = function(){
context.drawImage(image,50,50);
smear(context, 50, 100, 100, 100, 100);
};
縮放和反鋸齒:
過度縮放圖像可能會導致圖像模糊或像素化;可以通過使用2D上下文的imageSmoothingEnabled屬性來控制是否在縮放圖像時使用平滑算法;默認值為true,即啟用平滑縮放,也可以禁用此功能,如:
context.imageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
示例:zoom
<canvas id="canvas" width="300" height="227"></canvas>
<canvas id="zoom" width="300" height="227"></canvas>
<div>
<label for="smoothbtn">
<input type="checkbox" name="smoothbtn" checked="checked" id="smoothbtn">
Enable image smoothing
</label>
</div>
<script>
window.onload = function(){
var img = new Image();
img.src = 'images/3.jpg';
img.onload = function() {
draw(this);
};
function draw(img) {
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
img.style.display = 'none';
var zoomctx = document.getElementById('zoom').getContext('2d');
var smoothbtn = document.getElementById('smoothbtn');
var toggleSmoothing = function(event) {
zoomctx.imageSmoothingEnabled = this.checked;
zoomctx.mozImageSmoothingEnabled = this.checked;
zoomctx.webkitImageSmoothingEnabled = this.checked;
zoomctx.msImageSmoothingEnabled = this.checked;
};
smoothbtn.addEventListener('change', toggleSmoothing);
var zoom = function(event) {
var x = event.layerX;
var y = event.layerY;
zoomctx.drawImage(canvas,
Math.abs(x - 5),
Math.abs(y - 5),
10, 10,
0, 0,
200, 200);
};
canvas.addEventListener('mousemove', zoom);
}
}
</script>
imageSmoothingQuality屬性,用于設置圖像平滑度的屬性,一般配合imageSmoothingEnabled屬性使用;其可能的值為:"low","medium","high";
context.imageSmoothingQuality = "Medium";
命中檢測:
在2D繪圖上下文中,路徑是一種主要的繪圖方式,因為路徑能為要繪制的圖形提供更多控制,由于路徑的使用很頻繁,所以就有了一個名為isPointInPath(x, y[, fillRule])的方法,該方法接收x和y坐標作為參數,用于確定畫布上的某一點是否位于當前路徑上(內),該坐標是在默認坐標系中而不是在變換過的坐標系中;如:
context.rect(100, 100, 200, 100);
context.stroke();
if(context.isPointInPath(100, 100)){
alert("點(100,100)位于路徑內");
}
可選的參數fillRule,用來決定點在路徑內還是在路徑外的算法,允許的值:"nonzero": 非零環繞規則 ,默認的規則;"evenodd": 奇偶環繞原則;
isPointInPath()還有另外一種形式:isPointInPath(path, x, y[, fillRule]);參數path為一個Path2D路徑對象;
該方法用于命中檢測(hit detection):檢測鼠標單擊事件是否發生在特定的形狀上;但是,不是將MouseEvent對象的clientX和clientY屬性直接傳遞給isPointInPath()方法;首先,必須要將鼠標事件的坐標轉換成相應的畫布坐標;其次,如果畫布在屏幕上顯示的尺寸和實際尺寸不同,鼠標事件坐標必須要進行適當的縮放,如
function hitpath(context, event){
var canvas = context.canvas;
var rect = canvas.getBoundingClientRect();
var x = (event.clientX - rect.left) * (canvas.width / rect.width);
var y = (event.clientY - rect.top) * (canvas.height / rect.height);
return context.isPointInPath(x, y);
}
canvas.onclick = function(event){
if(hitpath(this.getContext("2d"),event)){
alert("Hit");
}
}
除了進行基于路徑的命中檢測之外,還可以使用getImageData()方法來檢測鼠標點下的像素是否已經繪制過了;如果返回的像素(單個或多個)是完全透明的,則表示該像素上沒有繪制任何內容,或者認為鼠標點空了,如:
function hitpaint(context, event){
var canvas = context.canvas;
var rect = canvas.getBoundingClientRect();
var x = (event.clientX - rect.left) * (canvas.width / rect.width);
var y = (event.clientY - rect.top) * (canvas.height / rect.height);
var pixels = context.getImageData(x, y, 1, 1);
for(var i=3; i<pixels.data.length; i+=4){
if(pixels.data[i] !== 0)
return true;
}
return false;
}
isPointInStroke([path,] x, y)方法:
用于檢測某點是否在路徑的描邊線上;參數:x、y為檢測點的 X 坐標Y 坐標,path為Path2D 路徑;當這個點在路徑的描邊線上,則返回 true,否則返回 false;
context.rect(10, 10, 100, 100);
context.lineWidth = 10;
context.stroke();
console.log(context.isPointInStroke(10, 10)); // true
console.log(context.isPointInStroke(12, 12)); // true
保存文件;
在畫布中繪制完成一幅圖形或圖像后,可以將該圖像或圖形保存到文件中;例如,用戶直接可以在畫布上右擊,把canvas繪圖保存為一個圖像文件,默認為PNG格式;
另外,也可以轉換為data URL,此時是把當前的繪畫狀態輸出到一個data URL地址所指向的數據中;
toDataURL(type [, quality])方法:可以把canvas上的繪制的圖像導出,參數type,為輸出數據的MIME類型;參數quality為圖像質量,值為從0到1,1 表示最好品質,0 基本不被辨析,但文件更小;
var imgURI = drawing.toDataURL("image/png");
var image = document.createElement("img");
image.src = imgURI;
document.body.appendChild(image);
默認情況下,瀏覽器會將圖像編碼為PNG格式,且分辨率為96dpi;
轉換為Blob對象:
也可以使用canvas對象的canvas.toBlob(callback, [mimeType[, qualityArgument]]);方法將canvas元素中的圖像直接轉換為一個Blob對象,參數mimeType指定圖像的MIME類型,當它參數值為“image/jpeg”或“image/webp”時,可以使用qualityArgument參數指定圖像質量,參數值為一個允許小數的數值,值范圍為0到1之間;當轉換成功時,會執行回調函數,其參數即為轉換成功的Blob對象,如:
<canvas id="canvas" width="400" height="300"></canvas><br/>
<input type="button" id="btnSave" value="保存圖像">
<script>
window.onload = function(){
draw('canvas');
var btnSave = document.getElementById("btnSave");
btnSave.addEventListener("click", savePic)
};
function draw(id){
var img = new Image();
img.src = "images/2.jpg";
img.onload = function(){
var canvas = document.getElementById(id);
if (canvas == null)
return false;
var context = canvas.getContext("2d");
context.drawImage(img, 0, 0, canvas.width, canvas.height);
}
}
function savePic(){
canvas.toBlob(function(blob){
var a = document.createElement("a");
a.textContent = "打開圖像";
document.body.appendChild(a);
a.style.display = "block";
a.href = URL.createObjectURL(blob);
},"image/png",0.95);
}
</script>
canvas元素的toBlob方法是非常重要的,因為如果使用Blob對象,可以使用Blob對象的size屬性獲取輸出后的文件尺寸;在將canvas元素中的圖像進行輸出或將其提交到服務器端時,如果使用Data URL,由于圖像數據為文本數據,對于大數據圖像,將大幅度增加瀏覽器端的負擔,如果使用Blob對象,由于瀏覽器內部使用二進制數據,會大幅度減輕瀏覽器的負擔;
解碼圖像:
針對一個cavnas元素來說,無論是修改、剪切或縮放其中的圖像后再利用時,首先要做的一件事情是解碼其中的圖像;問題在于當對canvas元素中的圖像進行解碼時,可能需要耗費較大的CPU資源;
使用window對象的createImageBitmap方法,可用于后臺解碼圖像,其返回一個ImageBitmap對象,開發者可以將該對象中存儲的圖像繪制到一個canvas元素中;
createImageBitmap(image[, sx, sy, sw, sh]).then(function(response) {…}); 參數image用于指定圖像來源,其可以為一個img元素、video元素、canvas、Blob、ImageData、ImageBitmap等;參數sx, sy, sw, sh分別用于指定被復制區域的起始坐標及寬和高;
該方法返回一個以一個ImageBitmap對象為結果的Promise對象,該對象中包含了指定區域的圖像;
var image = document.getElementsByTagName("img")[0];
image.onload = function(){
// var imageBitmap = window.createImageBitmap(image);
var imageBitmap = window.createImageBitmap(image, 50, 50, 200, 100);
console.log(imageBitmap); // Promise
}
例如,繪制一個圖像到畫布中
function draw(id){
fetch("images/1.jpg")
.then(response => response.blob())
.catch(error => console.error("Error:",error))
.then(response => {
let canvas = document.getElementById(id);
let context = canvas.getContext("2d");
createImageBitmap(response,50,50,400,300).then(
imageBitmap => context.drawImage(imageBitmap,0,0));
});
}
draw("canvas");
ImageBitmap對象:
ImageBitmap 接口表示能夠被繪制到 <canvas> 上的位圖圖像,具有低延遲的特性;一般由createImageBitmap()方法返回,并且它可以從多種源中生成,如img、canvas、video等;
ImageBitmap提供了一種異步且高資源利用率的方式來為WebGL的渲染準備基礎結構;
屬性:
width:只讀,無符號長整型數值,表示ImageData對象的寬度,單位為像素;
height:只讀,無符號長整型數值,表示ImageData對象的高度;
方法:
close():釋放ImageBitmap所相關聯的所有圖形資源;
可以在Web Worker中使用createImageBitmap方法;如果有許多圖像需要解碼,可以將URL傳遞給Web Worker,在其中下載并解碼圖像,然后將解碼結果傳遞給主線程以便將其繪制到canvas中;
worker.js代碼:
onmessage = function(event){
fetch(event.data).then(response => response.blob())
.catch(error => self.postMessage(error))
.then(response => {
createImageBitmap(response,23,5,57,80)
.then(imageBitmap => self.postMessage({imageBitmap:imageBitmap});)
});
}
js代碼:
function draw(id){
let canvas = document.getElementById(id);
let context = canvas.getContext("2d");
let worker = new Worker("worker.js");
worker.postMessage("images/1.jpg");
worker.onmessage = (evt) => {
if(evt.data.err)
console.log(evt.data.message);
context.drawImage(evt.data.imageBitmap,0,0);
};
}
createImageBitmap方法還有一個可選的options參數,其為一個設置選項的對象;可用的選項為:
// ...
createImageBitmap(response,50,50,400,300, {
imageOrientation:"flipY",
premultiplyAlpha:"premultiply",
colorSpaceConversion:"none",
resizeWidth:200,
resizeHeight:150,
resizeQuality:"medium"
}).then(
imageBitmap => context.drawImage(imageBitmap,0,0));
動畫的制作:
在Canvas畫布中制作動畫相對來說比較簡單,實際上就是一個不斷擦除、重繪、擦除、重繪的過程;
基本步驟:
操控動畫:
在繪圖圖形圖像時,僅僅在腳本執行結束后才能看見結果,所以,在類似for循環體里實現動畫是不太可能的;
因此,為了實現動畫,需要一些可以定時執行重繪的方法;即可以通過 setInterval、setTimeout 和window.requestAnimationFrame()方法來控制在設定的時間點上執行重繪,從而操控動畫;
如果并不需要與用戶互動,可以使用 setInterval() 方法;如果需要做一個游戲,可以使用鍵盤或者鼠標事件配合上setTimeout()方法來實現,通過設置事件監聽,可以捕捉用戶的交互,并執行相應的動作;
如:一個走動的小方塊
var context;
var w,h,i;
var timer = null;
function draw(id){
var canvas = document.getElementById(id);
if(canvas==null){
return false;
}
context = canvas.getContext("2d");
context.fillStyle = "#EEE";
context.fillRect(0,0,400,300);
w = canvas.width;
h = canvas.height;
i = 0;
timer = setInterval(rotate,100);
}
function rotate(){
if(i>= w -20)
clearInterval(timer);
context.clearRect(0,0,w,h);
context.fillStyle = "red";
context.fillRect(i,0,20,20);
i = i + 20;
}
draw("canvas");
示例:圖形組合變換
var globalId;
var i=0;
function draw(id){
globalId = id;
setInterval(Composite,1000);
}
function Composite(){
var canvas = document.getElementById(globalId);
if(canvas==null)
return false;
var context = canvas.getContext("2d");
var arr = new Array("source-atop","source_in","source-out","source-over","destination-atop","destination-in","destination-out","destination-over","lighter","copy","xor");
if(i>10) i=0;
context.clearRect(0,0,canvas.width,canvas.height);
context.save();
context.fillStyle = "blue";
context.fillRect(10,10,60,60);
context.globalCompositeOperation = arr[i];
context.beginPath();
context.fillStyle = "red";
context.arc(60,60,30,0,Math.PI*2,false);
context.fill();
context.restore();
i=i+1;
}
draw("canvas");
示例:太陽系的動畫
<canvas id="canvas" width="600" height="600"></canvas>
<script>
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init(){
sun.src = 'images/sun.png';
moon.src = 'images/moon.png';
earth.src = 'images/earth.png';
window.requestAnimationFrame(draw);
}
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.globalCompositeOperation = 'destination-over';
ctx.clearRect(0,0,300,300); // clear canvas
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.strokeStyle = 'rgba(0,153,255,0.4)';
ctx.save();
ctx.translate(150,150);
// Earth
var time = new Date();
ctx.rotate( ((2*Math.PI)/60)*time.getSeconds() + ((2*Math.PI)/60000)*time.getMilliseconds() );
ctx.translate(105,0);
ctx.fillRect(0,-12,50,24); // Shadow
ctx.drawImage(earth,-12,-12);
// Moon
ctx.save();
ctx.rotate( ((2*Math.PI)/6)*time.getSeconds() + ((2*Math.PI)/6000)*time.getMilliseconds() );
ctx.translate(0,28.5);
ctx.drawImage(moon,-3.5,-3.5);
ctx.restore();
ctx.restore();
ctx.beginPath();
ctx.arc(150,150,105,0,Math.PI*2,false); // Earth orbit
ctx.stroke();
ctx.drawImage(sun,0,0,300,300);
window.requestAnimationFrame(draw);
}
init();
</script>
示例:動畫時鐘
<canvas id="canvas" width="600" height="600"></canvas>
<script>
function clock(){
var context = document.getElementById('canvas').getContext('2d');
context.save();
context.clearRect(0,0,150,150);
context.translate(75,75);
context.scale(0.4,0.4);
context.rotate(-Math.PI/2);
context.strokeStyle = "black";
context.lineWidth = 8;
context.lineCap = "round";
// 小時刻度
context.save();
for (var i=0;i<12;i++){
context.beginPath();
context.rotate(Math.PI/6);
context.moveTo(100,0);
context.lineTo(120,0);
context.stroke();
}
context.restore();
context.save();
context.lineWidth = 5;
for (i=0;i<60;i++){
if (i%5 != 0) {
context.beginPath();
context.moveTo(117,0);
context.lineTo(120,0);
context.stroke();
}
context.rotate(Math.PI/30);
}
context.restore();
var now = new Date();
var sec = now.getSeconds();
var min = now.getMinutes();
var hr = now.getHours();
hr = hr>=12 ? hr-12 : hr;
context.fillStyle = "black";
context.save();
context.rotate(hr*(Math.PI/6) + (Math.PI/360)*min + (Math.PI/21600)*sec)
context.lineWidth = 14;
context.beginPath();
context.moveTo(-20,0);
context.lineTo(80,0);
context.stroke();
context.restore();
context.save();
context.rotate((Math.PI/30)*min + (Math.PI/1800)*sec)
context.lineWidth = 10;
context.beginPath();
context.moveTo(-28,0);
context.lineTo(112,0);
context.stroke();
context.restore();
context.save();
context.rotate(sec * Math.PI/30);
context.strokeStyle = "#D40000";
context.fillStyle = "#D40000";
context.lineWidth = 6;
context.beginPath();
context.moveTo(-30,0);
context.lineTo(83,0);
context.stroke();
context.beginPath();
context.arc(0, 0, 10, 0, Math.PI*2, true);
context.fill();
context.beginPath();
context.arc(95, 0, 10, 0, Math.PI*2, true);
context.stroke();
context.beginPath();
context.fillStyle = "rgba(0,0,0,1)";
context.arc(0,0,3,0,Math.PI*2,true);
context.fill();
context.restore();
context.beginPath();
context.lineWidth = 14;
context.strokeStyle = '#325FA2';
context.arc(0, 0, 142, 0, Math.PI*2, true);
context.stroke();
context.restore();
window.requestAnimationFrame(clock);
}
window.requestAnimationFrame(clock);
</script>
示例:循環全景照片
<canvas id="canvas" width="800" height="200"></canvas>
<script>
var img = new Image();
img.src = 'images/park.jpg';
var CanvasXSize = 800, CanvasYSize = 200;
var speed = 30;
var scale = 1.05;
var y = -4.5;
// 主程序
var dx = 0.75;
var imgW, imgH;
var x = 0;
var clearX, clearY;
var ctx;
img.onload = function() {
imgW = img.width * scale;
imgH = img.height * scale;
if (imgW > CanvasXSize) {
x = CanvasXSize - imgW; // -275.2
clearX = imgW;
} else {
clearX = CanvasXSize;
}
if (imgH > CanvasYSize) {
clearY = imgH;
} else {
clearY = CanvasYSize;
}
ctx = document.getElementById('canvas').getContext('2d');
setInterval(draw, speed);
}
function draw() {
ctx.clearRect(0, 0, clearX, clearY);
if (imgW <= CanvasXSize) {
if (x > CanvasXSize) {
x = -imgW + x;
}
if (x > 0) {
ctx.drawImage(img, -imgW + x, y, imgW, imgH);
}
if (x - imgW > 0) {
ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
}
} else {
if (x > CanvasXSize) {
x = CanvasXSize - imgW;
}
if (x > (CanvasXSize - imgW)) {
ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
}
}
ctx.drawImage(img, x, y, imgW, imgH);
x += dx;
}
</script>
示例:鼠標追蹤動畫
<style>
#cw {position: fixed; z-index: -1;}
body {margin: 0; padding: 0; background-color: rgba(0,0,0,0.05);}
</style>
<canvas id="cw"></canvas>
<script>
var cn;
//= document.getElementById('cw');
var c;
var u = 10;
const m = {
x: innerWidth / 2,
y: innerHeight / 2
};
window.onmousemove = function(e) {
m.x = e.clientX;
m.y = e.clientY;
}
function gc() {
var s = "0123456789ABCDEF";
var c = "#";
for (var i = 0; i < 6; i++) {
c += s[Math.ceil(Math.random() * 15)]
}
return c
}
var a = [];
window.onload = function myfunction() {
cn = document.getElementById('cw');
c = cn.getContext('2d');
for (var i = 0; i < 10; i++) {
var r = 30;
var x = Math.random() * (innerWidth - 2 * r) + r;
var y = Math.random() * (innerHeight - 2 * r) + r;
var t = new ob(innerWidth / 2,innerHeight / 2,5,"red",Math.random() * 200 + 20,2);
a.push(t);
}
//cn.style.backgroundColor = "#700bc8";
c.lineWidth = "2";
c.globalAlpha = 0.5;
resize();
anim()
}
window.onresize = function() {
resize();
}
function resize() {
cn.height = innerHeight;
cn.width = innerWidth;
for (var i = 0; i < 101; i++) {
var r = 30;
var x = Math.random() * (innerWidth - 2 * r) + r;
var y = Math.random() * (innerHeight - 2 * r) + r;
a[i] = new ob(innerWidth / 2,innerHeight / 2,4,gc(),Math.random() * 200 + 20,0.02);
}
// a[0] = new ob(innerWidth / 2, innerHeight / 2, 40, "red", 0.05, 0.05);
//a[0].dr();
}
function ob(x, y, r, cc, o, s) {
this.x = x;
this.y = y;
this.r = r;
this.cc = cc;
this.theta = Math.random() * Math.PI * 2;
this.s = s;
this.o = o;
this.t = Math.random() * 150;
this.o = o;
this.dr = function() {
const ls = {
x: this.x,
y: this.y
};
this.theta += this.s;
this.x = m.x + Math.cos(this.theta) * this.t;
this.y = m.y + Math.sin(this.theta) * this.t;
c.beginPath();
c.lineWidth = this.r;
c.strokeStyle = this.cc;
c.moveTo(ls.x, ls.y);
c.lineTo(this.x, this.y);
c.stroke();
c.closePath();
}
}
function anim() {
requestAnimationFrame(anim);
c.fillStyle = "rgba(0,0,0,0.05)";
c.fillRect(0, 0, cn.width, cn.height);
a.forEach(function(e, i) {
e.dr();
});
}
</script>
常規動畫設計:
繪制小球:
<canvas id="canvas" width="600" height="300"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var ball = {
x: 100,
y: 100,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
ball.draw();
</script>
添加速率并實現動畫:
<canvas id="canvas" width="600" height="300"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var rafID;
var ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function drawAnimation() {
ctx.clearRect(0,0, canvas.width, canvas.height);
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
rafID = window.requestAnimationFrame(drawAnimation);
}
canvas.addEventListener('mouseover', function(e){
rafID = window.requestAnimationFrame(drawAnimation);
});
canvas.addEventListener('mouseout', function(e){
window.cancelAnimationFrame(rafID);
});
ball.draw();
</script>
運動邊界:
// 在drawAnimation()函數中添加
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
ball.vx = -ball.vx;
}
rafID = window.requestAnimationFrame(drawAnimation);
設置加速度:
ball.vy *= .99;
ball.vy += .25;
// 添加到drawAnimation()函數中
ball.vy *= .99;
ball.vy += .25;
ball.x += ball.vx;
ball.y += ball.vy;
長尾效果:
// ctx.clearRect(0,0, canvas.width, canvas.height);
// 如:在drawAnimation中用以下代碼替代掉上方的clearRect()方法
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
ball.draw();
添加鼠標控制:
// ...
// 添加一個全局變量,用于判斷是否正在運動
var running = false;
// ...
function clear() {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
}
canvas.addEventListener('mousemove', function(e){
if(!running){
clear();
ball.x = e.offsetX;
ball.y = e.offsetY;
ball.draw();
}
});
canvas.addEventListener('mouseout', function(e){
window.cancelAnimationFrame(rafID);
running = false;
});
canvas.addEventListener('click',function(e){
if (!running) {
rafID = window.requestAnimationFrame(drawAnimation);
running = true;
}
});
// ...
使用 canvas 處理視頻:
通過canvas和video,可以實時地操縱視頻數據來合成各種視覺特效,并把結果呈現到視頻畫面中;
例如:色度鍵控(也被稱為“綠屏效果”)
<style>
body {background: black; color:#CCCCCC;}
#c2 {background-image: url(url("images/logo.png")); background-repeat: no-repeat;}
div {float: left; border :1px solid #444444;
padding:10px; margin: 10px; background:#3B3B3B;}
</style>
<div>
<video id="video" src="images/video.ogv" controls="true"/>
</div>
<div>
<canvas id="c1" width="160" height="96"></canvas>
<canvas id="c2" width="160" height="96"></canvas>
</div>
JavaScript:main.js
var processor = {};
processor.doLoad = function doLoad() {
this.video = document.getElementById('video');
this.c1 = document.getElementById('c1');
this.ctx1 = this.c1.getContext('2d');
this.c2 = document.getElementById('c2');
this.ctx2 = this.c2.getContext('2d');
var self = this;
this.video.addEventListener('play', function() {
self.width = self.video.videoWidth / 2;
self.height = self.video.videoHeight / 2;
self.timerCallback();
}, false);
},
主頁面:
<script src="video_main.js"></script>
<script>
window.onload = processor.doLoad();
</script>
實現計時器回調timerCallback()方法:
processor.timerCallback = function timerCallback() {
if (this.video.paused || this.video.ended) {
return;
}
this.computeFrame();
var self = this;
setTimeout(function() {
self.timerCallback();
}, 0);
},
實現computeFrame()方法用來操作視頻幀數據:
processor.computeFrame = function computeFrame() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
var frame = this.ctx1.getImageData(0, 0, this.width, this.height);
var l = frame.data.length / 4;
for (var i = 0; i < l; i++) {
var r = frame.data[i * 4 + 0];
var g = frame.data[i * 4 + 1];
var b = frame.data[i * 4 + 2];
if (g > 100 && r > 100 && b < 43)
frame.data[i * 4 + 3] = 0;
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
canvas 的優化:
避免浮點數的坐標點,應該使用整數:
當在繪制一個沒有整數坐標點的圖形時會發生子像素渲染;
ctx.drawImage(myImage, 0.3, 0.5);
瀏覽器為了達到抗鋸齒的效果會做額外的運算;為了避免這種情況,應該用Math.floor()函數對所有的坐標點取整;
盡量不要在用drawImage時縮放圖像:
因為瀏覽器一樣需要額外的運算,去處理縮放后的圖像;
避免使用多層畫布去畫一個復雜的場景:
在有些應用中,可能某些圖形需要經常移動或更改,而其他對象則保持相對靜態;在這種情況下,可以使用多個<canvas>元素對項目進行分層;
<style>
#stage {
width: 480px; height: 320px;
position: relative; border: 2px solid black
}
canvas { position: absolute; }
#ui-layer { z-index: 3 }
#game-layer { z-index: 2 }
#background-layer { z-index: 1 }
</style>
<div id="stage">
<canvas id="ui-layer" width="480" height="320"></canvas>
<canvas id="game-layer" width="480" height="320"></canvas>
<canvas id="background-layer" width="480" height="320"></canvas>
</div>
用CSS設置大的背景圖:
可以避免在每一幀在畫布上繪制大圖;
用CSS transforms特性縮放畫布:
因為CSS transforms使用 GPU,速度很快,所以最好的情況是不直接縮放畫布;
var scaleX = window.innerWidth / canvas.width;
var scaleY = window.innerHeight / canvas.height;
var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);
stage.style.transformOrigin = '0 0';
stage.style.transform = 'scale(' + scaleToFit + ')';
關閉透明度:
如果畫布不需要透明,當使用getContext()方法創建一個繪圖上下文時把 alpha 選項設置為 false,這個選項可以幫助瀏覽器進行內部優化;
var ctx = canvas.getContext('2d', { alpha: false });
其它建議:
CSDN 的讀者朋友們早上好哇,「極客頭條」來啦,快來看今天都有哪些值得我們技術人關注的重要新聞吧。
一分鐘速覽新聞點!
iPhone13出現紅綠雙色屏
亞馬遜在云計算SaaS領域未躋身前20,落后于微軟甲骨文等友商
微軟因捆綁銷售遭歐盟反壟斷起訴
谷歌警告挖礦者入侵Google Cloud賬戶
微軟股東投票批準每年發布性騷擾報告
國內要聞
抖音回應員工受賄被判刑:將嚴厲打擊內部貪腐
近日,字節跳動兩名員工王某迪與張某迎因非法收受他人財物57萬元,將指定內容推上抖音熱榜,法院一審判處兩人犯非國家工作人員受賄罪,其中王某迪被判有期徒刑一年二個月,罰金2萬元,張某迎被判有期徒刑一年,緩刑一年六個月,罰金2萬元。抖音表示,將進一步完善內部管理機制,嚴厲打擊內部貪腐,對其中涉嫌刑事犯罪的人員提交司法機關嚴懲。(紅星新聞)
騰訊官方回應:為什么QQ比微信更受年輕人歡迎
有媒體發文稱,微信成為人手必備的國民級社交軟件許多年后,QQ這個誕生于上個世紀的“老古董”,目前依然有可觀的用戶存量,始終占據自己的陣地。QQ最初的使用者是70后、80后們,彼時他們還都很年輕。這批人中有不少拋棄了QQ,但新一代的年輕人仍不斷涌入QQ。
昨晚,騰訊QQ官方微博也以#QQ為什么沒有被微信淘汰#進行了發聲,并表示“原來這么多QQ功能被鵝粉們喜歡著,今晚一定要給產品經理加雞腿!!!”(新浪科技)
小米公益平臺正式上線,多家基金會入駐
小米集團宣布小米公益平臺正式上線,公益平臺面向教育助學、緊急救災、鄉村振興、醫療救助等慈善捐贈領域,提供安全合規、精準高效的服務,探索互聯網慈善公益的新模式,助力中國慈善事業發展。
包括中國兒童少年基金會、 深圳壹基金公益基金會、北京感恩公益基金會、北京市海淀教育基金會、美麗中國支教項目、北京新陽光慈善基金會、春暉博愛、聯勸公益、滿天星公益、真愛夢想公益基金會等公益機構,已在第一期入駐小米公益平臺,上線慈善項目涉及教育助學、應急救災、特需教育等領域。(鈦媒體)
11月30日消息,多位網友反映PC端QQ出現閃退、無法登錄等問題,甚至有用戶稱,QQ一上午連續崩潰五次,都無法好好工作了。“QQ崩了”這個話題也一度沖上微博熱搜排行榜。對于此事,騰訊 QQ 官方微博回應:目前該問題已經修復。據悉,騰訊QQ在11月26日也出現過問題,部分用戶無法使用QQ登錄其他產品,包括 QQ 音樂、王者榮耀等,騰訊已經在當日修復該問題。(IT之家)
小米12有望拿下首批高通旗艦芯片驍龍8 Gen1
2021年驍龍技術峰會將于北京時間12月1日早上7點開幕。作為一年一度驍龍峰會的關注焦點,新一代驍龍旗艦芯片即將亮相。在今日稍晚時候高通發布的官方信息中,小米集團創始人、董事長兼首席執行官雷軍出現在合作伙伴嘉賓名單中。這似乎是小米想搶下高通驍龍最新旗艦芯片首發的信號,此前有爆料稱小米 12或將在 12 月底發布。
國際要聞
iPhone13現紅綠雙色屏
近日,有較多消費者投訴,iPhone手機出現綠屏現象,更是有iPhone 13出現了“左邊綠,右邊紅”的雙色屏。自iPhone 13系列發售以來,有不少消費者表示自己買的手機出現了不同程度的bug,包括間歇性觸控失靈,拍照有馬賽克、備份恢復bug、蜂窩數據自動開關、通話信號差等。
亞馬遜在云計算SaaS領域未躋身前20,落后于微軟甲骨文等友商
據報道,亞馬遜AWS在計算基礎設施領域占主導地位,包括提供給其他應用的計算和存儲能力,但AWS在云計算的另一個重要領域落后于微軟、Salesforce和甲骨文等競爭對手,這就是SaaS(軟件即服務)應用。在研究機構Synergy Research Group近期對SaaS市場的一項調查中,微軟、Salesforce、Adobe、甲骨文和SAP這前五大公司占據超過50%的市場份額,而AWS則未能躋身前20名。(新浪科技)
微軟因捆綁銷售遭歐盟反壟斷起訴
因云存儲系統捆綁銷售,微軟時隔多年再次在歐盟遭遇反壟斷爭議。云存儲公司Nextcloud與其他30多家歐洲軟件、云計算公司組成了名為“公平競爭環境聯盟”,針對微軟當下將其云存儲服務OneDrive、協作辦公軟件Teams和其他服務與Windows 10和Windows 11捆綁銷售的行為正式向歐盟委員會提起起訴。據公開資料顯示,微軟這幾十年來因為產品搭售,已經被歐盟罰了22億歐元,微軟的捆綁銷售壟斷行為由來已久(新浪科技)
谷歌警告挖礦者入侵Google Cloud賬戶
近日,谷歌警告說,加密貨幣“礦工”正在使用受損的Google Cloud賬戶進行計算密集型采礦。最近被盜用的50個Google Cloud賬戶中,有86%用于執行加密貨幣挖礦。加密貨幣挖礦通常需要大量的計算能力,Google Cloud客戶可以按成本使用這些計算能力。
微軟股東投票批準每年發布性騷擾報告
微軟股東周二批準一項提案,允許董事會發布與辦公場所性騷擾政策有效性有關的報告,激進人士的提案獲得批準相當少見。就在一年半之前,微軟創始人蓋茨離開董事會,因為有報道稱自2000年開始蓋茨就與一員工有染,隨后微軟董事會展開調查。微軟聲稱已經制定計劃,準備每年發布報告介紹騷擾歧視政策落實情況。
程序員專區
Spring Boot 2.6.1正式發布,主要是為了支持本周發布的Spring Cloud 2021.0。此版本包括11個錯誤修復和文檔改進。修復文檔 "External Application Properties" 部分中的拼寫錯誤;修復參考文檔 中 "spring --version" 的輸出;修復ErrorPageSecurityFilter部署到Servlet 3.1的兼容問題等。更新詳情查看鏈接:https://github.com/spring-projects/spring-boot/releases/tag/v2.6.1
WebStorm 2021.3正式發布,該版本是今年的最后一次重大更新。新的功能和改進包括:支持遠程開發、改進HTML補全和Deno集成;改進了對monorepos的支持、加快了 JavaScript文件的索引時間、快速修復了下載遠程ES6模塊的問題;更容易管理項目的依賴性、重新設計的Deno插件、支持Angular 13、更好的HTML補全等。更新詳情查看鏈接:https://www.jetbrains.com/webstorm/whatsnew/
擬器中的 Filter 或 Shader 基本都是基于圖像本身的。一般來說模擬器不會提供與幾何相關的 shader(32 位機以后會有少量這類 shader)。也就是說,模擬器濾鏡生成的圖像都是在不清楚游戲本身運行邏輯的情況下,單純對最終輸出的圖像進行變換。因此這里用 Filter 遠比用 Shader 來得更為精確。不過因為 RetroArch 的濾鏡系統將其稱為 Shader,因此之后將不分辨該用詞(shader = 濾鏡 = filter)。
以下將從抗鋸齒濾鏡、放大增強濾鏡、效果濾鏡和硬件仿真濾鏡四個角度對模擬器常用濾鏡進行介紹,并著重對現在應該如何模擬 CRT 進行說明。
抗鋸齒濾鏡
對模擬器常見的 2D 游戲,抗鋸齒濾鏡基本沒什么用,所以只是簡單介紹一下。
首先是為什么要抗鋸齒。大家知道時域采樣往往要用規則采樣。時域采樣在頻域中相當于用狄拉克梳子卷積信號本身,如果被采樣信號的帶寬低于采樣信號的奈奎斯特頻率,就沒問題,不然就會堆疊失真產生 aliasing(一維叫混疊,二維叫鋸齒)。在空間域中采樣幾何本身或者現實世界圖片的時候,規則采樣用得很少,因為很容易對周期性高頻信號出現 aliasing。人們通過局部改進分辨率、隨機采樣等等途徑進行抗鋸齒,就產生了各種 AA 算法。
我們知道模擬器濾鏡都是作用于屏幕空間(不是模擬器圖形設置中的 AA 選項),和圖形渲染不同:它往往是通過減少圖像中的高頻信號,而非增加采樣頻率或改變采樣策略進行 AA 的。是純粹的初次采樣完畢之后的空間域行為,不需要獲知圖形的幾何信息。
常用的屏幕空間 AA 就是 FXAA 了,其具體原理太過繁瑣,可參考此貼:
https://catlikecoding.com/unity/tutorials/advanced-rendering/fxaa/
一般來說,2D 游戲,尤其 16 位機器以下的游戲不要使用 Anti-aliasing shader。像素圖像本身甚至可以說就是由鋸齒構成的,如果強行進行 AA 會使圖像看起來非常詭異:
3D 游戲可酌情使用,尤其是模擬器本身 AA 開的不高的情況下。屏幕空間的 AA 效果雖然一般但通常速度較快,如果開 3D 游戲模擬器內 AA 比較吃力的情況下,就湊合用屏幕空間的 AA 吧。
放大增強濾鏡
這類濾鏡是平時最常見的,也是人們最為經常使用的濾鏡(雖然 LZ 并不常用這類濾鏡)。它的主要作用是減少像素畫面的顆粒感。像素藝術最大的問題就是經不起放大:一旦放大以后,原本可愛的 Sprite 瞬間變得猙獰了起來:
為了解決像素圖像放大的問題,人們發明了一系列增強算法。在機器學習介入之前,這類濾鏡還比較簡單,我們也只考慮機器學習之前的常用濾鏡。
首先是基本的插值:Nearest Neighbor,Bilinear 兩種。
像素圖片放大這件事上,只要模擬器輸出分辨率跟具體顯示分辨率不匹配,模擬器本身就要選擇一種插值方式。可以進行線性插值(顏色設為鄰居的加權平均)或者最近鄰插值(與最近的鄰居像素顏色相同)。顯示上最近鄰插值能夠還原原本的像素顆粒,而線性插值能進行初步的模糊和潤滑,具體喜歡哪種就看個人喜好了。
Scale 系列:
包括 Scale2x、2xSal、EPX、AdvMAME2x 等等。這類濾鏡是使用簡單的 filter 對圖像進行卷積。有時比單純的卷積要復雜一些,等價于使用了多個不同的濾鏡進行卷積以后產生多個圖像,最后對圖像進行條件混合。還有些強調邊緣的濾鏡也會通過圖像的二次差分判斷邊緣從而采取不同的混合策略(權重)。
考慮最簡單的 Scale2x,將像素 P 放大為 4 個子像素,根據周圍 4 像素設置子像素的顏色采用以下規則:
1=P; 2=P; 3=P; 4=P;
IF C==A => 1=A
IF A==B => 2=B
IF D==C => 3=C
IF B==D => 4=D
IF of A, B, C, D, three or more are identical: 1=2=3=4=P
則稱之為 Scale2x 算法。其結果其實基本上就是把一個像素分成了四個像素,顆粒感會大大下降。規則簡單,性能也好:
其它 Scale 系列同理,主要都是在放大之后根據原圖像周圍像素顏色通過一定規則決定子像素顏色。
Eagel、2xSal 也是同一系列的濾鏡,只不過考慮的周圍像素范圍不同。例如比較復合的 Super2xSal 考慮的像素范圍就要更大一些,涉及周圍 11 個像素的值,并且也設置了相似的判定規則,效果如下:
Scale 系列濾鏡是我認為 16 位機和 8 位機的底線,下面的就稍微有點越界了。
HQx 和 xBR 濾鏡系列
有時候現代人口味刁鉆,希望能消除像素本身的顆粒感。而前面那些簡單的臨像素加權平均或分支的濾鏡會導致邊緣模糊,并且處理像素游戲中的線條非常苦手,因此有人開發了相應更復雜的濾鏡滿足這些人的需求。
HQx 系列 :(high-quality scale)
這一系列濾鏡會根據周圍像素顏色與自己的不同關系(周圍 8 個像素根據閾值分為相似或者不相似兩類,因此共 256 種可能),通過查找表的方式確定放大之后的像素顏色如何定義。而這一查找表本身的定義比較復雜。用 C 寫幾千行也很不容易(包含了 HQx-2/3/4 https://github.com/grom358/hqx/blob/master/src/hq2x.c),當然用 GLSL 要簡單不少。其目的主要是為了放大之后的線條能夠更加順滑。
xBR 濾鏡系列
xBR 濾鏡系列,包含 xBR , xBRZ, xBR-Hybrid, Super xBR, xBR+3D 和 Super xBR+3D.
同樣的,這些濾鏡也主要是用來游玩像素游戲時消除像素顆粒感使用。總有人認為這種圓滑感看起來比顆粒感的像素更舒服一些。
這類濾鏡比 HQx 更強大的地方在于通過多個 pass 解決了許多 HQx 的單次查找表索無法解決的問題,讓還原的線條更加銳利。
具體原理參考:( https://forums.libretro.com/t/xbr-algorithm-tutorial/123)( https://pastebin.com/cbH8ZQQT)
雖然這里可憐的馬里奧看起來有點不堪,但一般情況下這個濾鏡沒有那么慘。xBR 濾鏡對邊緣的處理遠比 HQx 更加強大,非常善于消除像素的顆粒感并且保留色塊和邊緣的銳利。NGA 有人專門寫過一篇文章吹這個濾鏡,并且認為 xBRZ 是最好的 2D 放大濾鏡(單純從放大角度,不考慮深度學習類的方法,應該算是沒錯的)。有需求的可以參考一下:
https://bbs.nga.cn/read.php?tid=9171524
(然而說實話我是 xBR PTSD,看著就難受)
其他大多數像素增強也都是采用了各種不同的自定規則對子像素進行插值。效果有好有壞。游戲之間的圖像特征也有很大的區別,適用不同濾鏡,大家使用時可以根據自己的視覺體驗進行選擇。
比如 NEDI(New Edge-Directed Interpolation), 論文: http://web.archive.org/web/20101126091759/http://neuron2.net/library/nedi.pdf
比如專門為 GB/GBA 設計的 OmniScale: https://sameboy.github.io/scaling/
深度學習方面尤其跟 GAN 有關的方法則包含一些 AI 將圖片庫中特征結合進行的創作,不符合高還原度 retro gaming 的主題,一般也不推薦使用。
效果型濾鏡
以下濾鏡會生成一些有趣的效果,一般用不著,想體驗一下也行。
Dithering:dithering 是早期 PC、針式打印機等等用點陣表示密度來展現色彩的。最近很火的獨立游戲《obra dinn》也是這種風格。但單純基于圖像的 dithering 其實很消耗時間,采用一些近似的化效果也不好。用在 16 位機的游戲上也并不合適,湊合看看吧:
bayer-matrix-dithering:
Cel-shading:卡通渲染用在 16 位機上當然是個災難,但 3D 游戲有時候也有點意思。同樣的,不要指望單純的屏幕空間的濾鏡能搞出什么花來:
老電影:
technicolor 濾鏡是一個不錯的老電影效果濾鏡,還能模擬膠卷上的點和劃痕:
效果型濾鏡隨喜好添加即可。
硬件仿真型濾鏡
這是我認為模擬器屏幕空間濾鏡真正有用的地方,也是本帖的重頭戲。這類濾鏡的目標是盡量模擬真實硬件的顯示設備,在現代 LED 顯示器上對古舊顯示設備(掌機屏幕、電視、街機 CRT 等)進行仿真,從而帶來更多模擬游戲和懷舊樂趣的一類濾鏡。
注意,這里介紹的大部分濾鏡的最佳使用場景都是 4K 顯示器全屏游玩。各種掌機屏幕幾乎都沒有能力模擬這些效果,而手機屏幕太小,效果是看不清的。
首先來說說比較簡單的掌上設備。使用顯示器屏幕模擬掌機設備的一大問題是無法準確模擬掌機屏幕的表現。而屏幕空間的濾鏡通過色彩、像素顆粒感這兩方面嘗試逼近掌機屏幕的表現。
例如 GB(帶光)式的色彩和像素映射(gameboy-light):仔細觀看會發現馬里奧采用了方形像素,并且使用了橫向和縱向的像素分割線對老式 LCD 進行了風格化。色彩映射也是 GB 的綠屏。
可見像素并非簡單近鄰插值,而是同時模擬了像素本身的熒光擴散效果。使用了大量的像素模擬了單個 GBA 像素的熒光擴散灰度顯示不同亮度時的不同梯度。因此才能將像素顯示的陰影感準確模擬出來。而這一效果也是在 4K 顯示器下才能體現得最為明顯。因為 4K 顯示器有足夠的像素去表現這些效果。(你要問我為什么用 4K 顯示器全屏玩 GB 游戲?可能是吃得太飽了……)
對比一下就知道加濾鏡和不加濾鏡的天壤之別。很可愛吧,是不是想起了另一個古舊 LCD 設備(文曲星):
這里這個 GB 的濾鏡是 LCD 系列濾鏡的一種。這一系列濾鏡就是為了創造相應掌機設備 LCD 屏幕效果而出現的。它的原理大框架就是增加像素之間的間隔形成 LCD 顆粒感,通過隔離的熒光過渡形成像素本身的陰影感,從而復原當年的古舊 LCD 屏幕的樣子。
比如 GBA 樣式(包括了 GBA 屏幕的顏色映射,GBA 反射顏色并不鮮艷,用現代的屏幕去顯示需要通過一定的映射)。一定程度上還原了像素排列方式,甚至還原了 GBA 屏幕本身的動態模糊:
舉個 GBA 游戲的例子:用 GBA 的朋友對這種色彩和像素風格的畫面應該有印象
對比不加濾鏡的鮮艷色彩和線性插值的圖像 (用模擬器玩曉月的朋友記憶中應該是這個畫面):
如果你覺得 9102 年了還要還原 GBA 的色彩簡直開歷史倒車(雖然這正是這篇文要干的事情……),那么也可以只進行 LCD 像素映射。只使用 LCD3X 系列濾鏡而不映射色彩即可:
同樣的,NDS 也可以采用類似濾鏡。
由于 PSP 的屏幕相對來說要好不少,類似現代顯示器屏幕,一般沒有針對 PSP 屏幕的模擬需求。如果想模擬 PSP 的屏幕可以使用 RetroArch 自帶的 PSP-color 進行色彩映射。
下面說說另一個(真正的)重頭戲:
CRT 濾鏡
首先請把所有其它 CRT 濾鏡扔掉,只留下一個:CRT-Royale(除非硬件跑不了,再考慮其他)。
濾鏡使用了大量 pass 進行了 CRT 的模擬。如果 PC 性能夠強的話,延時方面的影響也很小。CRT-Royale 十分復雜和強大, 對 GPU 有一定的要求。如果用 intel 的 GPU 的話(集顯)需要進行修改,改版也在 RetroArch 里提供了。
用來顯示 CRT-royale 濾鏡的屏幕至少需要 2K 以上的分辨率,4K 甚至 8K 屏幕的模擬效果更加真實。是的你沒看錯,要模擬 CRT,最低要求是 2K 分辨率,4K 更佳。
我們知道 CRT 中的磷光體(或熒光體)是產生冷發光現象的物質,受到陰極射線(電子束)激活發光。它發出的光線具有一定的特征,與現代 LED 的像素光線有較大的區別。CRT 濾鏡的關鍵就是通過大量現代 LED 像素去模擬磷光體的發光特征,從而模擬 CRT 的顯示效果。而在這方面做得最好的就是此濾鏡了。(CRT 雖然沒有直接的像素的概念,只有熒光粉或者熒光條。不過電子束的信息改變是離散的,因此我們可以將離散電子束信息改變周期內掃過的空間等價為像素的概念)。
在 RetroArch 的桌面 UI 里打開 CRT-royale 的設置界面,我們可以看到很多相關設置,涉及到一些重要的調整項。如果你對 Shader 語言略有了解,也可以直接打開 Shader 文件進行調整,只是沒有界面中方便。根據每個人接觸到的不同型號和不同廠家生產的 CRT,你所喜愛的 CRT 參數必然有所不同,玩家可以自行調整到喜歡的設置選項。
首先看看效果(網絡圖片有壓縮,要觀看大體效果還是自己 4K 全屏運行模擬器比較靠譜。看圖片也要看大圖,小圖自帶 AA,把所有特征都抹掉了):
對比沒開濾鏡的游戲:
影響最終效果的選項很多。下面我們來解釋一些影響較大的參數:
Halation and Diffusion
Halation 是被熒光體直接反射的光線,而 Diffusion 是光線穿過 CRT 玻璃時產生的散射熒光。這兩項參數的權重可以進行調整。
Bloom
如果點亮的熒光體發光過強影響到了電視上的其他面積,使整個畫面變得過亮,就是一種 bloom 的效果。特別好的 CRT 會控制 bloom,但由于這是大量中低端電視可能產生的效果,因此也需要忠實模擬。
Beam
這項參數控制了實際進行掃描的電子束的各項維度。不知為何一直有人認為 scanline 是黑線:scanline 是掃描到的線,而沒掃描到的地方才是黑線。除了可以調整 Beam 本身的大小以外,這里也可以調整高斯模糊函數的各個參數。根據不同的參數選擇可能產生不同型號電視或街機的效果:
Convergence
彩色電視電子槍發射的三束射線對熒光粉的轟擊是否足夠整齊:好的 CRT 比如彩監是非常整齊的,但許多消費者級別的 CRT 這方面的表現就很一般了,根據每個人童年不同質量的 CRT 可以仔細微調。
MASK
這項控制的是熒光體的排列方式。濾鏡提供了三種排列:0.0 (Aperture Grille), 1.0 (Slot Mask), 和 2.0 (Dot Mask)。這三種排列如下:
每一種排列都對應不同廠家的電視效果,可以分別予以調整。同時,MASK 也有大量參數可以進行調整。比如使用的熒光體個數可以調整 CRT 顯示的粒度。
和其它濾鏡相比也是高下立判。如果你覺得沒有高下立判,就調整參數讓它高下立判!
不同的制式和不同的輸入會有一定程度的圖像失真,沒關系,這些失真可以用額外的 pass 來模擬。比如電視機的 composite 輸入導致的色彩失真效果,加 NTSC 的色彩映射的效果如下:
再傳兩個其他游戲的圖,還是那句話,要在自己的屏幕上運行模擬器動態才能比較明顯看到效果。
以上基本介紹了常見的幾種 Filter 和它們的大體效果。那么如何使用這些 filter?哪些模擬器支持 shader 語言寫的 filter 呢?
這里:http://emulation.gametechwiki.com/index.php/Shaders_and_Filters 介紹了一些常見模擬器支持的 filter 文件類型。一般來說,采用通用前端 RetroArch 可以使用大部分的 shader,而使用模擬器自帶前端則有很多限制。所以最簡單的方案就是直接使用 RetroArch,然后從其 shader 文件夾中選擇所需 shader,并通過菜單調整相應參數即可。
總結:模擬器濾鏡是個挺大的話題,這里只是簡單介紹一些類型和它們的效果。一般來說,對 32 位及以下機型模擬時才推薦使用屏幕空間的濾鏡,抗鋸齒濾鏡一般不推薦像素游戲使用。像素放大增強濾鏡根據個人口味使用,一般來說進行簡單的 2xSal 等即可,特別討厭像素的顆粒感的話,可以考慮 HQx 或 xBR 系列濾鏡。如果追求一些特殊效果,可以使用效果型濾鏡。而為了模擬古舊硬件(主要是顯示設備),則可以分別使用該種硬件的濾鏡。CRT 濾鏡主要就是 CRT-Royale,按照自己口味調整之后,配合高亮度 4K 顯示器,基本可以滿足一般 CRT 模擬需求。如果有特殊需要當然實機 + 彩監更好,沒有這個條件的話模擬器效果也不賴,而且彩監只能體驗一種或少數型號的顯示效果,而濾鏡可以自行配制體驗多種電視和信號的不同感覺,因此也并不沖突。此外,部分 Filter 會降低游戲性能,或者因為需要幀信息從而略微增加游戲延時,有性能需求時應當關閉所有濾鏡。
不摸魚了。
參考:
http://emulation.gametechwiki.com/index.php/CRT-Royale
https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms
https://www.retroarch.com/index.php?page=shaders
http://emulation.gametechwiki.com/index.php/Shaders_and_Filters
作者:Lunamos@tgfc
原文鏈接:https://bbs.tgfcer.com/thread-7657428-1-1.html
*請認真填寫需求信息,我們會在24小時內與您取得聯系。