整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

第76节 Canvas绘图(下)-前端开发之JavaScript-王唯

上节

绘制文本:
可以在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);

在进行文字绘制之前,可以先对该对象的有关文字绘制的属性进行设置,如:

  • font属性:设置字体、大小和样式,与CSS中font属性一致;
  • textAlign:设置水平对齐方式,值有:start、end、left、right、center;建议使用start和end,不要使用left和right,因为start和end更加的稳妥,能同时适合从左到右和从右到左显示(阅读)的语言;
  • textBaesline:设置文本基线,值有:top、hanging、middle、alphabetic、ideographic(表意的)、bottom,默认值是alphabetic;

这三个属性都有默认值,因此也不是必须显式去设置它们;

// 使用字体样式
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属性:用来在绘制文本时,描述当前文本方向的属性,可能的值:

  • ltr:文本方向从左向右;
  • rtl:文本方向从右向左;
  • inherit:继承,默认值;
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还定义了多个有关字体的属性:

  • fontKerning属性:对应CSS的font-kerning,设置是否使用字体中储存的字距信息;
  • fontStretch属性:对应CSS的font-stretch,为字体定义一个正常或经过伸缩变形的字体外观,这个属性并不会通过伸展/缩小而改变字体的几何外形;
  • fontVariantCaps属性:对应CSS的font-variant-caps,使用不同的大写形式;
  • letterSpacing属性:对应CSS的letter-spacing,设置字间距;
  • wordSpacing属性:对应CSS的word-spacing,设置词间距;
  • textRendering属性:对应CSS的text-rendering,如何渲染字体;

示例:绘制带有文字的饼形图

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属性:
表示后绘制的图形怎样与先绘制的图形结合,其可能的值如下:

  • normal:默认值,不混合;
  • darken:变暗模式,保留两个图层中最暗的像素;
  • lighten:变亮模式,保留两个图层中最亮的像素;
  • multiply:正片叠底,像素被倒转,相乘,再倒转,结果是一幅更明亮的图片;
  • screen:滤色模式,像素被倒转,相乘,再倒转,结果是一幅更明亮的图片;
  • color-burn:颜色加深模式,将反置的底层除以顶层,然后将结果反过来;
  • color-dodge:颜色减淡模式,将底层除以顶层的反置;
  • hard-light:强光模式,屏幕相乘,类似于叠加,但上下图层互换了;
  • soft-light:柔光模式,用顶层减去底层或者相反来得到一个正值;
  • overlay:叠加模式,multiply 和 screen 的结合,原本暗的地方更暗,原本亮的地方更亮;
  • difference:差值模式,一个柔和版本的强光(hard-light);纯黑或纯白不会导致纯黑或纯白;
  • exclusion:排除模式,和 difference 相似,但对比度较低;
  • hue:保留了底层的亮度(luma)和色度(chroma),同时采用了顶层的色调(hue);
  • saturation:保留底层的亮度(luma)和色调(hue),同时采用顶层的色度(chroma);
  • color:保留了底层的亮度(luma),同时采用了顶层的色调 (hue) 和色度 (chroma);
  • luminosity:保持底层的色调(hue)和色度(chroma),同时采用顶层的亮度(luma);
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属性去指定组合方式,其可能的值如下:

  • source-over:默认值,表示将新图形绘制在源图形上,对于半透明的源图形就直接合并,这是默认值;
  • source-in:新图形与源图形做in运算,只显示新图形中与源图形相重叠的部分,新图形与源图形的其他部分均变成透明;
  • source-out:新图形与源图形做out运算,只显示新图形中与源图形不重叠的部分,新图形与源图形的其他部分均变成透明;
  • source-atop:只绘制新图形中与源图形相重叠的部分以及未被重叠覆盖的源图形,新图形的其他部分变成透明;
  • destination-over:表示在源图形之下绘制新图形;
  • destination-in:源图形与新图形做in运算,只显示源图形中与新图形相重叠的部分,新图形与源图形的其他部分均变成透明;
  • destination-out:新图形与源图形做out运算,只显示源图形中与新图形不重叠的部分,新图形与源图形的其他部分均变成透明;
  • destination-atop:只绘制源图形中被新图形重叠覆盖的部分及新图形的其他部分,源图形的其他部分变成透明,且新图形在源图形后面绘制;
  • lighter:源图形与新图形均绘制,重叠部分做加色处理;
  • xor:只绘制新图形中与源图形不重叠的部分,重叠部分变成透明;
  • copy:只绘制新图形,源图形中未与新图形重叠的部分变成透明;
  • source-over、destination-over和copy是三种常用的合成类型;另外,如果指定的值不在这几个字符串中,则按默认的方式组合图形;
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功能;有两个可能值:

  • anonymous:当使用这个值时,就会在请求中的header中的带上Origin属性,但请求不会带上cookie和其他的一些认证信息;
  • use-credentials:当使用这个值时,会同时在跨域请求中带上cookie和其他的一些认证信息;

默认情况下,如果没有使用这个属性,说明没有开启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参数,其为一个设置选项的对象;可用的选项为:

  • imageOrientation:指示图像是按原样呈现还是垂直翻转,值为none不翻转或flipY翻转;
  • premultiplyAlpha:指示位图颜色通道由alpha通道预乘,值为none、premultiply或default;
  • colorSpaceConversion:指示图像是否使用色彩空间转换进行解码,值为none或default;
  • resizeWidth:指定新宽度;
  • resizeHeight:指示新高度;
  • resizeQuality:指定图像质量,值为:pixelated、low (默认)、medium或high;
// ...
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画布中制作动画相对来说比较简单,实际上就是一个不断擦除、重绘、擦除、重绘的过程;

基本步骤:

  • 1)清空canvas:预先编写好用来绘图的函数,在该函数中先用clearRect方法将画布整体或局部擦除;
  • 2)保存状态:如果要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧的时候都是原始状态的话,需要先保存一下;
  • 3)绘制动画图形:使用setInterval方法设置动画的间隔时间;
  • 4)恢复状态;

操控动画:
在绘图图形图像时,仅仅在脚本执行结束后才能看见结果,所以,在类似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 });

其它建议:

  • 将画布的函数调用集合到一起;
  • 避免不必要的画布状态改变;
  • 渲染画布中的不同点,而非整个新状态;
  • 尽可能避免使用shadowBlur;
  • 尽可能避免使用text rendering
  • 有动画,尽量使用requestAnimationFrame()而非setInterval();

CSDN 的读者朋友们早上好哇,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。


整理 | 孙胜
出品 | CSDN(ID:CSDNnews)

一分钟速览新闻点!


  • 抖音回应员工受贿被判刑:将严厉打击内部贪腐

  • 腾讯官方回应:为什么QQ比微信更受年轻人欢迎

  • 小米公益平台正式上线,多家基金会入驻

  • “QQ 崩了”上热搜,腾讯回应:问题已经修复

  • 小米12有望拿下首批高通旗舰芯片骁龙8 Gen1

  • iPhone13出现红绿双色屏

  • 亚马逊在云计算SaaS领域未跻身前20,落后于微软甲骨文等友商

  • 微软因捆绑销售遭欧盟反垄断起诉

  • 谷歌警告挖矿者入侵Google Cloud账户

  • 微软股东投票批准每年发布性骚扰报告

  • Spring Boot 2.6.1发布

  • WebStorm 2021.3发布


国内要闻


抖音回应员工受贿被判刑:将严厉打击内部贪腐

近日,字节跳动两名员工王某迪与张某迎因非法收受他人财物57万元,将指定内容推上抖音热榜,法院一审判处两人犯非国家工作人员受贿罪,其中王某迪被判有期徒刑一年二个月,罚金2万元,张某迎被判有期徒刑一年,缓刑一年六个月,罚金2万元。抖音表示,将进一步完善内部管理机制,严厉打击内部贪腐,对其中涉嫌刑事犯罪的人员提交司法机关严惩。(红星新闻)

腾讯官方回应:为什么QQ比微信更受年轻人欢迎

有媒体发文称,微信成为人手必备的国民级社交软件许多年后,QQ这个诞生于上个世纪的“老古董”,目前依然有可观的用户存量,始终占据自己的阵地。QQ最初的使用者是70后、80后们,彼时他们还都很年轻。这批人中有不少抛弃了QQ,但新一代的年轻人仍不断涌入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 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发布

WebStorm 2021.3正式发布,该版本是今年的最后一次重大更新。新的功能和改进包括:支持远程开发、改进HTML补全和Deno集成;改进了对monorepos的支持、加快了 JavaScript文件的索引时间、快速修复了下载远程ES6模块的问题;更容易管理项目的依赖性、重新设计的Deno插件、支持Angular 13、更好的HTML补全等。更新详情查看链接:https://www.jetbrains.com/webstorm/whatsnew/


微软 Edge “不务正业”,新功能遭用户抵制:“你是在抢钱吧?”

拟器中的 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